Skip to content
Beta: the Biskèt Partner API may still change before v1 is final. Sandbox access is by request:partners@bisket.ai.

Webhooks

Register an endpoint with POST /v1/webhook_endpoints (url, enabled_events, optionally location_ids). The answer carries the endpoint’s signing secret (whsec_…) once. URLs must be public https.

Every delivery is an event: id, type, created_at, livemode, location_id, object_type, object_id, sequence, request_id, and data.object (the full object after the change), data.previous_attributes, data.details. Every event type and payload is in the reference under Webhooks.

Bisket-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"). Verify on the raw body, accept any matching v1 (two appear while a secret rotation overlaps), and reject a t more than 300 s away.

webhook-receiver.ts
// A webhook receiver: verify the signature on the RAW body, answer 2xx fast, work after.
// BISKET_WEBHOOK_SECRET=whsec_… npx tsx webhook-receiver.ts
import { createServer, type Server } from 'node:http';
import { verifyWebhook, WebhookVerificationError } from '@bisket-ai/partner-sdk';
export function receiver(secret: string, onEvent: (type: string, orderId?: string) => void): Server {
return createServer(async (req, res) => {
const chunks: Buffer[] = [];
for await (const c of req) chunks.push(c as Buffer);
const raw = Buffer.concat(chunks).toString('utf8'); // never JSON.parse before verifying
try {
const event = await verifyWebhook(raw, req.headers as Record<string, string>, secret);
res.writeHead(204).end(); // acknowledge within 10 s; process asynchronously
// Delivery is at least once: de-duplicate on event.id, order by event.sequence per object.
if (event.type.startsWith('order.')) onEvent(event.type, (event.data.object as { id: string }).id);
else onEvent(event.type);
} catch (e) {
res.writeHead(e instanceof WebhookVerificationError ? 400 : 500).end();
}
});
}
if (process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop()!)) {
receiver(process.env.BISKET_WEBHOOK_SECRET!, (type, id) => console.log(type, id ?? '')).listen(3000);
}

These four are tested in their own runtimes in our CI, against real signed deliveries.

  • At least once. De-duplicate on id.
  • Answer any 2xx within 10 seconds, then process. Redirects are not followed.
  • Retries after 30 s, 1 min, 2 min, 5 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h, then every 8 h, for 72 hours; then the event is dead-lettered.
  • 410 disables the endpoint at once.
  • An endpoint that fails for 24 h with 50 failures in a row is disabled automatically; you and Biskèt are notified.
  • Ordering: none across objects. Within one object, events arrive in sequence order in the normal case; ignore an event whose sequence is not greater than the last one you applied for that object.
  • Replay: GET /v1/events lists events (30 days); POST /v1/events/{event_id}/redeliver sends one again.
  • Secret rotation: POST /v1/webhook_endpoints/{id}/rotate_secret returns a new secret; the old one keeps signing alongside it during the overlap.