Webhook Delivery
When an event you subscribed to occurs, Basis Theory sends it to your webhook URL as an HTTP POST request. This page describes that request, how to verify and acknowledge it, and how retries work. The event object inside it is described on Event Types, and the same objects can be read later through Query Events.
Request
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
BT-SIGNATURE | Base64-encoded signature of the request body. See Verifying the Signature. |
BT-SIGNATURE-VERSION | v1 for Production Tenants or v1-test for Test Tenants. Selects the key that verifies BT-SIGNATURE. |
BT-TRACE-ID | Correlation identifier for the delivery. Include it when contacting support about a delivery. |
BT-WEBHOOK-CALLBACK | true on every delivery. |
Body
The body wraps one event object with the time of this delivery attempt.
{
"event": {
"id": "e99b3bb9-25e4-484f-8a6d-6bc28edbbf39",
"type": "token.created",
"timestamp": "2024-04-09T13:56:37.864Z",
"tenant_id": "869d5b1c-1ae8-4ce6-96c6-73a602b407ff",
"trace_id": "0f777425-9080-4d82-b557-799f43aefcba",
"data": {
"token": { ... },
"actor": {
"id": "...",
"type": "...",
"name": "..."
}
}
},
"delivered_at": "2024-04-09T13:56:42.673Z"
}
| Name | Type | Description |
|---|---|---|
event | object | The event. Its fields are the same on every delivery and are described in Structure of Events; the keys under data depend on event.type. |
delivered_at | string | ISO 8601 timestamp of this delivery attempt. A retried event is sent with a new delivered_at and a new signature. Because it is inside the signed body, a replayed request cannot carry a fresh value without failing signature verification. |
Keys in the body are sorted alphabetically at every level, and the signature is computed over that serialization. Do not rely on key order when reading the payload.
Verifying the Signature
Basis Theory signs every delivery so you can confirm it came from Basis Theory and was not altered in transit. Verify the signature on every request and reject deliveries with a missing or invalid signature before processing them.
The body is signed with a Basis Theory private key, and you verify it with the public key for the version named in the BT-SIGNATURE-VERSION header:
v1, for Production Tenants: https://cdn.basistheory.com/keys/webhooks.v1.keyv1-test, for Test Tenants: https://cdn.basistheory.com/keys/webhooks.v1-test.key
The signature is in the BT-SIGNATURE header, encoded as base64. It covers the body serialized with alphabetically sorted keys, so parse the body and serialize it the same way before verifying, as the sample below does.
const crypto = require('crypto');
const stringify = require('json-stable-stringify');
const verifyWebhook = (webhookRequest) => {
const body = JSON.parse(webhookRequest.request.body);
const signature = webhookRequest.request.headers['BT-SIGNATURE'];
const publicKey = ''; // Retrieve the signing key from https://cdn.basistheory.com/keys/webhooks.v1.key
const verifier = crypto.createVerify('SHA256');
verifier.update(stringify(body));
const isVerified = verifier.verify(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
},
Buffer.from(signature, 'base64')
);
return isVerified;
};
If your receiver accepts webhooks from both Production and Test Tenants, use the BT-SIGNATURE-VERSION header to select the correct key. See Webhook Signatures for an example of dynamic verification across both versions.
v1-test key must only be used to verify webhooks from Test Tenants. Production webhooks will continue to use the v1 signature and key.Acknowledging a Delivery
Respond with any 2xx status within 25 seconds. Any other status, or no response in time, counts as a failed attempt. A 2xx stops retries, so store or enqueue the delivery before you respond and process it afterwards. If you cannot persist it in time, return a non-2xx status so Basis Theory retries.
Retries
A delivery is attempted up to 10 times per event and webhook URL. The first retry follows about 10 seconds after a failed attempt, and the delay doubles on each retry up to 15 minutes, with random jitter. Only the webhook URLs that failed are retried; a URL that already acknowledged the event does not receive it again.
Duplicate Deliveries
Webhooks are delivered at least once: retries and other conditions can send the same event more than once. Make processing idempotent, using event.id as the idempotency key, so that handling the same event twice has no unintended side effects. delivered_at identifies the attempt and is what you check to guard against replay attacks.
Replay Attacks
To guard against replay attacks, verify the signature first, then compare delivered_at with the current time and reject deliveries that are too old. The timestamp is part of the signed body, so a replayed request cannot be given a newer one without invalidating its signature. Idempotent processing limits the effect of any replay that gets through.
Disabled Webhooks
An event that fails all 10 attempts to a webhook URL counts as one exhausted delivery for that URL. When a URL accumulates 10 exhausted deliveries within an hour, Basis Theory disables it and emails the address in its notify_email attribute. A disabled webhook stays disabled until you re-enable it with the Update Webhook API.