Skip to contentSkip to content

Receive webhooks

The platform sends outbound events containing only required metadata. Signing follows this contract:

ItemValue
X-SignatureLowercase hexadecimal HMAC-SHA256
Signed bytesUTF-8 bytes of ${X-Webhook-Timestamp}.${exact raw body}
X-Webhook-TimestampUnix seconds
X-Webhook-Event-IdStable UUID idempotency key
X-Webhook-SequenceAppClient-scoped, monotonically increasing positive integer

Required processing order

  1. Read exact raw request bytes under a size limit; do not parse JSON first.
  2. Validate required headers and the time window. The official SDK defaults to 300 seconds of clock skew.
  3. Read the signing secret from a Secret Manager, calculate HMAC-SHA256 over the exact bytes, and compare in constant time.
  4. Parse JSON only after successful verification and confirm the body event_id equals the header.
  5. Atomically claim the event ID in durable storage before business effects. Return 2xx for a duplicate without repeating effects.
  6. 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.