Skip to main content

Webhooks

Instead of polling, register an HTTPS endpoint and receive a POST for every event in your operation — charge paid, agreement accepted, dispute resolved.

Registration

Manage endpoints under Settings → Webhooks (or via POST /api/v1/webhook-endpoints), picking the subscribed events — an empty list subscribes to all of them, including future events. Each resource emits *.created, *.updated, *.deleted and lifecycle events (charge.paid, agreement.accepted, dispute.resolved...). The full catalog, with a sample payload per event, lives in Events.

Delivery

Each event fires a POST with the JSON payload:

POST /your-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Kobana-Dunning-Webhooks/1.0
X-Webhook-Event: charge.paid
X-Webhook-Event-Id: 3d1f0c9a-5b2e-4c7d-9e1f-8a6b4c2d0e9f
X-Webhook-Delivery-Id: 9b2e4a10-7c3f-4d8e-a1b2-c3d4e5f60718
X-Webhook-Attempt: 1
X-Webhook-Timestamp: 2026-07-23T12:00:00.000Z
X-Webhook-Signature: t=1753272000,v1=<hmac-sha256 of "t.body" with your secret>

{
"event": "charge.paid",
"timestamp": "2026-07-23T12:00:00.000Z",
"organizationId": "cmd8k0a1b0001ef5g",
"data": { ...resource serialized in the v1 API format... }
}

Your endpoint can require its own authentication (Basic, Bearer or a custom header), configured at registration. Deduplicate by X-Webhook-Event-Id (stable across attempts and redeliveries).

Verifying the signature

X-Webhook-Signature comes in the format t=<unix>,v1=<hex>: v1 is the HMAC-SHA256 of "{t}.{raw body}" with the endpoint's secret. Check the timestamp tolerance (recommended: 5 minutes — anti-replay) and compare in constant time:

const crypto = require('crypto');

const TOLERANCE_SECONDS = 300; // 5 minutes

function verify(rawBody, signatureHeader, secret) {
const t = /(?:^|,)t=(\d+)(?:,|$)/.exec(signatureHeader || '')?.[1];
const v1 = /(?:^|,)v1=([0-9a-f]+)(?:,|$)/i.exec(signatureHeader || '')?.[1];
if (!t || !v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - Number(t)) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1.toLowerCase(), 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Reject (with 401) any delivery whose signature doesn't match.

Test event

POST /api/v1/webhook-endpoints/{id}/test delivers a real webhook.test event through the same pipeline (queue, HMAC signature, capture, retry) — validate the consumer end to end before the first business event.

Consumer contract

  • Respond 2xx immediately and process asynchronously — the timeout is 10 seconds per attempt.
  • Unknown event? Respond 200 and ignore it — new events may start being emitted without notice.
  • Deduplicate: delivery is "at least once"; a 200 lost on the network triggers a resend.

Failed deliveries are retried 8 times with exponential backoff (4-minute base, total window ≈ 8h30); the full history, with captured request and response and manual redelivery, lives on the endpoint's page. Schedule and contract details in Deliveries and retry and signature verification in Security.