CAPSEND API Docs
ASYNC UPDATES

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

EventMeaning
transaction.processedThe transfer completed successfully.
transaction.failedThe transfer failed. Use the status endpoint when you need the recorded failure reason.
refund.processedAn approved refund or reversal completed.
json
{
  "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

HeaderMeaning
x-webhook-event-idStable event ID. Store it and process it only once.
x-webhook-delivery-attemptCurrent delivery-attempt number.
x-webhook-signatureHMAC SHA-256 signature in sha256=<hex> format.
x-webhook-timestampUnix timestamp in seconds used to create the signature.
x-webhook-idIdentifier for the configured webhook.

Verify every request

Build the signed value from the timestamp, a period, and the exact raw request body:

text
<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.

javascript
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

  1. Read the raw body and webhook headers.
  2. Verify the timestamp and signature.
  3. Confirm the header event ID matches payload id.
  4. Insert the event ID into durable storage with a uniqueness constraint.
  5. If it already exists, return 200 without applying the transfer result again.
  6. Commit your state change, then return any HTTP 2xx within 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.

Copyright CAPSEND Finance Services Ltd
Search documentation Find an endpoint, workflow, or integration rule.

Start typing to search the integration guide.