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.
The event
Section titled “The event”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.
Verify the signature
Section titled “Verify the signature”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.
// A webhook receiver: verify the signature on the RAW body, answer 2xx fast, work after.// BISKET_WEBHOOK_SECRET=whsec_… npx tsx webhook-receiver.tsimport { 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);}// Node 18+ — Biskèt webhook signature verification (portal sample, as published// in docs/api/partner-api-design.md §5.10). Pass the RAW request body.import crypto from 'node:crypto';export function verifyBisket(rawBody, header, secret, toleranceSec = 300) { const pairs = header.split(',').map(p => { const i = p.indexOf('='); return [p.slice(0, i), p.slice(i + 1)]; }); const t = Number(pairs.find(([k]) => k === 't')?.[1]); if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); return pairs.filter(([k]) => k === 'v1').some(([, v]) => { const got = Buffer.from(v, 'hex'); return got.length === expected.length && crypto.timingSafeEqual(got, expected); });}# Python 3.9+ — Biskèt webhook signature verification (portal sample, as# published in docs/api/partner-api-design.md §5.10). Pass the RAW body bytes.import hmac, hashlib, timedef verify_bisket(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool: items = [p.split("=", 1) for p in header.split(",")] ts = [v for k, v in items if k == "t"] if not ts or abs(time.time() - int(ts[0])) > tolerance: return False expected = hmac.new(secret.encode(), f"{ts[0]}.".encode() + raw_body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, v) for k, v in items if k == "v1")<?php // PHP 8+ — Biskèt webhook signature verification (portal sample, as// published in docs/api/partner-api-design.md §5.10). Pass the RAW request body.function verify_bisket(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { $t = null; $sigs = []; foreach (explode(',', $header) as $part) { [$k, $v] = array_pad(explode('=', $part, 2), 2, ''); if ($k === 't') { $t = (int) $v; } elseif ($k === 'v1') { $sigs[] = $v; } } if ($t === null || abs(time() - $t) > $tolerance) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); foreach ($sigs as $s) { if (hash_equals($expected, $s)) { return true; } } return false;}These four are tested in their own runtimes in our CI, against real signed deliveries.
Delivery
Section titled “Delivery”- At least once. De-duplicate on
id. - Answer any
2xxwithin 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.
410disables 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
sequenceorder in the normal case; ignore an event whosesequenceis not greater than the last one you applied for that object. - Replay:
GET /v1/eventslists events (30 days);POST /v1/events/{event_id}/redeliversends one again. - Secret rotation:
POST /v1/webhook_endpoints/{id}/rotate_secretreturns a new secret; the old one keeps signing alongside it during the overlap.