Webhooks
CAPSEND sends a signed HTTPS POST request when a transfer reaches a terminal result. Staging and production webhook URLs and secrets are configured separately.
Events
| Event | Meaning |
|---|---|
transaction.processed | The transfer completed successfully. |
transaction.failed | The transfer failed. Use the status endpoint when you need the recorded failure reason. |
refund.processed | An approved refund or reversal completed. |
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "transaction.processed",
"createdAt": "2026-10-25T09:00:06.000Z",
"data": {
"transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"reference": "CAP-8E5A19E0D3394E55A3CE4FBC876A514B",
"amount": "100.0000",
"currency": "USD"
}
}
Delivery headers
| Header | Meaning |
|---|---|
x-webhook-event-id | Stable event ID. Store it and process it only once. |
x-webhook-delivery-attempt | Current delivery-attempt number. |
x-webhook-signature | HMAC SHA-256 signature in sha256=<hex> format. |
x-webhook-timestamp | Unix timestamp in seconds used to create the signature. |
x-webhook-id | Identifier for the configured webhook. |
Verify every request
Build the signed value from the timestamp, a period, and the exact raw request body:
<timestamp>.<raw_body>
Compute HMAC SHA-256 using your webhook secret, prefix the lowercase hexadecimal result with sha256=, and compare it with x-webhook-signature in constant time. Reject requests more than five minutes from your server time.
import crypto from 'node:crypto';
export function verifyCapsendWebhook(rawBody, headers, secret) {
const signature = headers['x-webhook-signature'];
const timestamp = headers['x-webhook-timestamp'];
if (typeof signature !== 'string' || !/^\d+$/.test(timestamp ?? '')) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = `sha256=${crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex')}`;
const received = Buffer.from(signature);
const calculated = Buffer.from(expected);
return received.length === calculated.length
&& crypto.timingSafeEqual(received, calculated);
}
Handle an event
- Read the raw body and webhook headers.
- Verify the timestamp and signature.
- Confirm the header event ID matches payload
id. - Insert the event ID into durable storage with a uniqueness constraint.
- If it already exists, return
200without applying the transfer result again. - Commit your state change, then return any HTTP
2xxwithin 10 seconds.
Webhook delivery is at least once. CAPSEND keeps the same event ID across retries and approved replays, so duplicate delivery must be harmless.
Retries
CAPSEND retries timeouts, connection failures, and non-2xx responses. Redirects are not followed. Automatic attempts are bounded; unresolved failures are reviewed operationally.
For event types your integration does not yet recognize, safely store the verified event, return 2xx, and alert your team. This avoids repeated delivery while preserving the payload for review.