Skip to navigation

Webhooks

A webhook endpoint is an HTTPS URL on your server. Sinbad sends it signed events about your customers’ trades and messages, so you do not have to poll. Delivery is at least once, so your endpoint must verify each signature and ignore repeats.

Confirm with Sinbad that webhook delivery is available in your environment. Registering an endpoint and queuing a test do not prove that delivery works.

1. Register an endpoint

Call POST /v1/webhooks. The URL must:

  • use https on port 443,
  • be a public hostname with no credentials, query string or fragment,
  • not be an IP address or a reserved local hostname, and
  • resolve in DNS to public addresses only.

Sinbad does not follow redirects. Register the endpoint for one permitted integration, and store its signing secret immediately: Sinbad returns it once, and it is not returned again if you repeat the request with the same Idempotency-Key. List your endpoints with GET /v1/webhooks.

2. Verify ownership

Call POST /v1/webhooks/{webhookId}/test. Sinbad queues a webhook.test event. Verify its signature against the raw request body, and respond with the exact challenge in a JSON response. A 202 from the test call means the test is queued, not that it succeeded. Check the result with GET /v1/webhooks/{webhookId}.

3. Receive trade and message events

Verify every signature, store the event durably, and deduplicate on the event ID. Each event type has a page with the complete payloads:

Verify signatures before parsing

Use the exact raw request bytes and the timestamp header. Decode the whsec_ suffix of your secret as base64url, then compute HMAC-SHA256 over timestamp + "." + rawBody. Match an accepted key ID, and reject timestamps more than 5 minutes from your clock.

import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(rawBody, timestamp, signatureHeader, secrets, now = Date.now()) {
if (!/^\d{1,12}$/.test(timestamp) || Math.abs(now / 1000 - Number(timestamp)) > 300)
return false;
return signatureHeader.split(',').some(entry => {
const match = /^v1=([\w-]{1,64}):([a-f0-9]{64})$/.exec(entry.trim());
if (!match || !secrets[match[1]]) return false;
const key = Buffer.from(secrets[match[1]].slice(6), 'base64url');
const expected = createHmac('sha256', key)
.update(timestamp).update('.').update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(match[2], 'hex'));
});
}

After verification:

  1. Require the signed event ID to match the Sinbad-Webhook-Id header.
  2. Check the expected environment and integration.
  3. Save a durable inbox record before you acknowledge the event.

A duplicate of an event you already accepted can be acknowledged again.

4. Inspect and replay failures

Read deliveries and their attempts with GET /v1/webhooks/{webhookId}/deliveries and GET /v1/webhooks/{webhookId}/deliveries/{deliveryId}. Fix your endpoint, then replay an eligible failed event with POST /v1/webhooks/{webhookId}/deliveries/{deliveryId}/replay within 30 days of when Sinbad captured it. A replay keeps the original event ID and body.

Sinbad retries a delivery up to 12 attempts or 24 hours, whichever comes first, when it hits a timeout, a network failure, or a 408, 429 or 5xx response. Any other failure is final. A 410 response disables the endpoint.

5. Manage the endpoint

Change an endpoint with PATCH /v1/webhooks/{webhookId}, using the current revision. Rotate its secret with POST /v1/webhooks/{webhookId}/rotate-secret, either with a 24-hour overlap or as an immediate replacement. Pause an endpoint, or remove it with DELETE /v1/webhooks/{webhookId}, when you no longer need it.

A successful test moves a disabled endpoint to paused. Activate it explicitly after you have verified it. Limits for these calls are in Limits and limitations.