Webhooks

Learn how to set up your webhook endpoint, configure authentication, and receive Driver Events from Suntransfers.

Endpoint requirements

  • An absolute https:// URL with a valid certificate.
  • The host name resolves to a public IP address. Local, private and reserved addresses (for example localhost, 10.0.0.0/8 or 192.168.0.0/16) are not reached.
  • The endpoint accepts POST requests with a JSON body.
  • The endpoint answers directly. Redirects (3xx) are not followed.

Set up your webhook

  1. Choose an authentication method. Review the supported options in Webhook Authentication.
  2. Provide your webhook details. Share your endpoint URL, preferred authentication method, and required configuration details with your Suntransfers contact.
  3. Test in Staging. Once your webhook has been configured, verify that your endpoint can receive and process Driver Events.
  4. Go live. After successful testing, coordinate with your Suntransfers contact to configure your webhook in Production.

Separate configurations are required for Staging and Production.

Webhook Authentication

Values each method needs:

MethodRequiredOptional
No auth——
API key in query paramparam, value—
Custom headerheader, value—
Basic authusername, password—
OAuth2 client credentialstoken_url, client_id, client_secretscope, audience
HMAC payload signatureheader, secretalgorithm

Rules for these values:

  • Every value is non-empty text.
  • header is a standard HTTP header name: letters, digits and hyphens, with no spaces or :. It can't be a content header such as Content-Type or Content-Length.
  • The value of a custom header is a single line of printable ASCII text.
  • username can't contain :.
  • token_url follows the same rules as your endpoint: an absolute https:// URL whose host resolves to a public IP address.
  • algorithm is sha256 (the default) or sha512, in lowercase.

Authentication Methods

No Authentication

The request carries no credentials. Use it only if your endpoint is protected in another way.

API Key in Query Param

You provide a parameter name (param) and a value (value):

{
  "param": "key",
  "value": "<api key>"
}

Both are URL-encoded and added to the query of your URL. If your URL already has a query, the parameter is added after the existing ones with &.

POST /driver-events?key=abc123 HTTP/1.1
Host: webhooks.partner.example.com
POST /driver-events?source=suntransfers&key=abc123 HTTP/1.1
Host: webhooks.partner.example.com
Custom Header

You provide a header name (header) and its value (value). The header is sent exactly as you give it, and replaces any header with the same name.

{
  "header": "Authorization",
  "value": "Bearer <token>"
}
POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
Authorization: Bearer <token>

To send an API key in a header, use a name such as X-API-Key with the bare key as the value:

POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
X-API-Key: abc123
Basic Authentication

You provide a username and a password. Each request carries an Authorization header with the Base64 encoding of username:password.

{
  "username": "partner-user",
  "password": "partner-pass"
}
POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
Authorization: Basic cGFydG5lci11c2VyOnBhcnRuZXItcGFzcw==
OAuth2 Client Credentials

You provide the URL of your token endpoint (token_url), a client_id and a client_secret, and optionally a scope and an audience.

{
  "token_url": "https://auth.example.com/oauth/token",
  "client_id": "my-client-id",
  "client_secret": "my-client-secret",
  "scope": "driver-events",
  "audience": "driver-events-api"
}

Token request. Suntransfers gets an access token from your token endpoint with the client credentials grant:

  • a POST to token_url with a form-encoded body: grant_type=client_credentials, plus scope and audience when you provide them;
  • the client authenticates with HTTP Basic: client_id and client_secret are URL-encoded, joined with : and Base64-encoded.
POST /oauth/token HTTP/1.1
Host: auth.example.com
Authorization: Basic bXktY2xpZW50LWlkOm15LWNsaWVudC1zZWNyZXQ=
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=driver-events&audience=driver-events-api

Token response. It must be a JSON object with access_token. expires_in is the token's lifetime in seconds, as a number or a numeric string.

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Using the token. Each request carries the token in an Authorization header:

POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
  • The token is reused until shortly before it expires. Without expires_in, a token is used for one request only.
  • If your endpoint answers 401, a new token is requested and the event is sent once more. If that request also gets a 401, the event is not delivered.
  • If your token endpoint rejects the credentials or its response has no access_token, the event is not delivered.
HMAC Payload Signature

You provide a header name (header), a shared secret (secret) and, optionally, the algorithm (algorithm: sha256, the default, or sha512).

{
  "header": "X-Signature",
  "secret": "<secret>",
  "algorithm": "sha256"
}

Each request carries the signature of its body in that header:

POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
Content-Type: application/json; charset=utf-8
X-Signature: 887c469c3d0d44ad40f495c4586d07db143406216c61fcaacabdf0dadf46d23c

Verifying the HMAC signature

How the signature is computed:

  • Signed content: the exact bytes of the request body, as received.
  • Key: the UTF-8 bytes of the shared secret.
  • Algorithm: HMAC-SHA256, or HMAC-SHA512 if agreed.
  • Header value: the digest in lowercase hexadecimal, with no prefix: 64 characters for SHA-256, 128 for SHA-512.

To verify a request:

  1. Read the raw request body before parsing the JSON. Parsing and serializing it again can change the bytes and break the signature.
  2. Compute the HMAC of the raw body with your secret.
  3. Compare it with the header value using a constant-time comparison.
  4. If they don't match, reject the request with 401.
const crypto = require('crypto');

// rawBody: the request body exactly as received (Buffer), before parsing the JSON
function isValidSignature(rawBody, signature, secret, algorithm = 'sha256') {
  const expected = crypto.createHmac(algorithm, Buffer.from(secret, 'utf8')).update(rawBody).digest('hex');
  const received = Buffer.from(signature || '', 'utf8');
  return received.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), received);
}
import hashlib
import hmac

ALGORITHMS = {"sha256": hashlib.sha256, "sha512": hashlib.sha512}


# raw_body: the request body exactly as received (bytes), before parsing the JSON
def is_valid_signature(raw_body: bytes, signature: str, secret: str, algorithm: str = "sha256") -> bool:
    expected = hmac.new(secret.encode("utf-8"), raw_body, ALGORITHMS[algorithm]).hexdigest()
    return hmac.compare_digest(expected, signature or "")
using System.Security.Cryptography;
using System.Text;

// rawBody: the request body exactly as received, before parsing the JSON
static bool IsValidSignature(byte[] rawBody, string? signature, string secret, string algorithm = "sha256")
{
    var key = Encoding.UTF8.GetBytes(secret);
    var hash = algorithm switch
    {
        "sha256" => HMACSHA256.HashData(key, rawBody),
        "sha512" => HMACSHA512.HashData(key, rawBody),
        _ => throw new ArgumentException($"Unsupported algorithm: {algorithm}", nameof(algorithm))
    };

    var expected = Encoding.ASCII.GetBytes(Convert.ToHexStringLower(hash));
    var received = Encoding.ASCII.GetBytes(signature ?? "");
    return CryptographicOperations.FixedTimeEquals(expected, received);
}

Test vector

Use these values to check your implementation:

Value
Secretmy-shared-secret
Body[{"bookingReference":"SUNTR_AB1234","externalReference":"AGENCY_REF_1","transferPosition":2,"eventType":"DRIVER_ARRIVED_AT_PICKUP","occurredAt":"2026-07-08T18:10:00+02:00","latitude":41.38,"longitude":2.17}]
Signature (sha256)887c469c3d0d44ad40f495c4586d07db143406216c61fcaacabdf0dadf46d23c
Signature (sha512)a7c1b26c6530df3cc5ee4aabeb7976875ff6f15523d9ce6ea6bcc90b4f2b712ce874cc79c551dc04aaa674e4a8b0745d2bb4ed9842e076b3a781eb1d61642c2e

Request format

Each event is sent as a POST request to your endpoint:

  • Content-Type: application/json; charset=utf-8
  • The body is a JSON array of events, as described in Receive Driver Events, sent as compact JSON with no spaces or line breaks.
POST /driver-events HTTP/1.1
Host: webhooks.partner.example.com
Content-Type: application/json; charset=utf-8
Content-Length: 207

[{"bookingReference":"SUNTR_AB1234","externalReference":"AGENCY_REF_1","transferPosition":2,"eventType":"DRIVER_ARRIVED_AT_PICKUP","occurredAt":"2026-07-08T18:10:00+02:00","latitude":41.38,"longitude":2.17}]

The request also carries the header or query parameter of your authentication method, as shown below.

Responding to events

  • Answer with any 2xx status to confirm that you received the event. The response body is ignored.
  • Answer quickly and process the event afterwards, for example by storing it and handling it in the background.