Verifying webhooks
Every notification carries two independent signatures. Before you act on a notification, verify them and reject the request with 401 if either required check fails.
Both signatures are calculated over the exact bytes of the request body. Capture the raw body before parsing or re-serialising the JSON, and verify against that raw string. Parsing first can change whitespace and break verification.
Layer 1 — Svix delivery signature (situational)
This is a cheap local check — it requires no outbound call and completes in microseconds. It proves the HTTP request came from RTGS.global's delivery infrastructure and is not a replay of an earlier delivery. Verify this first: if it fails, reject immediately without spending a network round-trip on the RTGS.global signature check.
The signature is carried in three HTTP headers:
| Header | Description |
|---|---|
svix-id | Unique delivery identifier (same on retries). |
svix-timestamp | Unix timestamp (seconds) of the delivery. |
svix-signature | HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw-body}, base64-encoded, sent as v1,<signature>. |
The timestamp is checked against a ±5 minute window to prevent replays.
When to verify the Svix signature
Whether you need it depends on how your endpoint is exposed:
- Public endpoint (reachable from the internet): verify it. This is the common case and the safe default. The Svix check rejects spoofed and replayed requests before you make any outbound call.
- Private endpoint, reachable only by RTGS.global (e.g. behind the VPN / private link, with mutual TLS terminating at your edge): you may relax the Svix check, because transport authenticity is already enforced by the network and certificate layer. Verifying it anyway is still recommended as defence in depth.
In all cases, Layer 2 remains mandatory — relaxing the Svix check never means skipping the RTGS.global signature.
Get your signing secret
The signing secret is generated when your endpoint is registered. RTGS.global provides you with four values — a server URL, an appId, an endpointUid, and a management token — and you use them to retrieve the whsec_... secret from the Svix management API. Fetch it at application startup; the secret changes if the endpoint is rotated or re-created. Store the retrieved value in a secrets manager and never commit it to source control.
Verify with the Svix SDK
The official Svix SDKs compute and compare the signature for you, including the timestamp window. Pass the raw request body and the three svix-* headers. The SDK throws on any failure — respond 401 when it does.
Verifying a Svix Delivery Signature →
If no Svix SDK is available for your stack, compute the HMAC yourself: base64-decode the secret (the part after whsec_), compute HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw-body}, base64-encode the result, and compare it (constant-time) against each value in the svix-signature header. Also reject deliveries whose svix-timestamp is outside a ±5 minute window. See the Svix manual verification guide for the exact algorithm.
Layer 2 — RTGS.global message signature (mandatory)
This signature proves the message was genuinely produced by RTGS.global and that the payload has not been altered in transit. It is always mandatory and requires an outbound call to the signing service — which is why you check the Svix signature first to gate unnecessary calls.
To verify it:
- Take the
verificationmaterialfield from the CloudEvent as the signature. - Base64-decode the
data_base64field to recover the exact payload string, and use that as the message. - Call the signing service verify endpoint. If the response is not
Verified: true, reject the notification with401and log a security alert.
The signature's context component identifies who the message was signed for. The Verifying an RTGS Message Signature recipe shows how to decode the context and confirm it was addressed to your RTGS.global ID.
Verifying an RTGS Message Signature →
Authenticating deliveries (optional)
For publicly reachable endpoints you can ask RTGS.global to authenticate to you on every delivery using OAuth 2.0 client credentials. RTGS.global fetches a short-lived bearer token from your authorization server before each delivery and sends it as an Authorization: Bearer <token> header.
To enable this, provide the following during onboarding:
| Value | Description |
|---|---|
| Token endpoint URL | Your authorization server's token endpoint (e.g. your identity provider's /token URL). |
| Client ID | Issued by your authorization server. |
| Client secret | Issued by your authorization server. |
| Scopes (optional) | Space-separated scopes, e.g. webhooks.receive. |
Your endpoint then validates the bearer token (signature, expiry, audience, issuer) in addition to verifying the Svix signature — OAuth authenticates the caller, it does not replace signature verification. This works with any standards-compliant identity provider (for example Microsoft Entra ID, Auth0, Okta, or Keycloak).
Order of checks
- Read the raw body and the
svix-*headers. - Verify the Svix signature (where applicable) → reject
401on failure. - Verify the RTGS.global signature → reject
401on failure. - Only now parse and process the payload — see Processing the payload.
Aborting retries on signature failure
When your endpoint rejects a delivery because signature verification fails, Svix will retry the request on its normal backoff schedule. Because a bad signature cannot be fixed by retrying the same payload, those retries are pointless and waste resources on both sides.
To stop Svix retrying a specific message, include the webhook-delivery response header alongside your 401:
HTTP/1.1 401 Unauthorized
webhook-delivery: abort-message
This tells Svix to abandon retries for that message only. Future deliveries continue normally.
| Header value | Effect |
|---|---|
webhook-delivery: abort-message | Stop retrying this specific message. |
webhook-delivery: disable | Immediately disable the endpoint — all future deliveries stop. |
abort-message, not disabledisable stops all future deliveries to your endpoint permanently. Do not return it on a signature failure. Only use disable if you are intentionally decommissioning an endpoint.