Receive webhooks
The platform sends outbound events containing only required metadata. Signing follows this contract:
| Item | Value |
|---|---|
X-Signature | Lowercase hexadecimal HMAC-SHA256 |
| Signed bytes | UTF-8 bytes of ${X-Webhook-Timestamp}.${exact raw body} |
X-Webhook-Timestamp | Unix seconds |
X-Webhook-Event-Id | Stable UUID idempotency key |
X-Webhook-Sequence | AppClient-scoped, monotonically increasing positive integer |
Required processing order
- Read exact raw request bytes under a size limit; do not parse JSON first.
- Validate required headers and the time window. The official SDK defaults to 300 seconds of clock skew.
- Read the signing secret from a Secret Manager, calculate HMAC-SHA256 over the exact bytes, and compare in constant time.
- Parse JSON only after successful verification and confirm the body
event_idequals the header. - Atomically claim the event ID in durable storage before business effects. Return 2xx for a duplicate without repeating effects.
- Complete safe work or enqueue it and return 2xx within 10 seconds.
ts
const event = await verifier.verify(request.headers, rawBody)
await enqueueSafeEffect(event.event_id, event.type)
return new Response(null, { status: 204 })The TypeScript SDK's in-memory replay store is for local tests only. Production must implement a durable, atomic claim(eventID). During rotation, accept the previous secret only until an explicit expiry.
Non-2xx responses, connection failures, and timeouts trigger exponential-backoff retries for up to 24 hours. Replays retain the original event ID and sequence. Never copy a secret, signature, or full event body into an error response, log, queue, or crash report.