Webhook delivery is asynchronous. A hold or booking does not wait for your receiver, and delivery order across independent occurrences is not guaranteed. Treat each verified message as a fact that may arrive late or more than once.
Delivery contract
- Each HTTP attempt has a five-second timeout.
- Any
2xxacknowledges the delivery. - Non-
2xxresponses and transport failures are retryable. - The queue allows the first attempt plus up to three retries.
- Exhausted messages leave the automatic path; attempts remain inspectable.
- One failing subscription can cause the shared occurrence to retry, so an already successful endpoint must still deduplicate.
Scoped delivery
An occurrence reaches a subscription only when the subscribed event name and both routing axes match.
| Subscription mode | Environment | Receives |
|---|---|---|
null |
null |
Every matching event |
live |
null |
Live events in any environment |
live |
prod |
Live events created in prod |
test |
staging |
Test events created in staging |
A null subscription field is a wildcard. Event environment is stamped from
the secret key used to create that event. Payload livemode remains available
as a second handler guard.
Idempotent processing
Use occurrenceId as the primary deduplication key. Keep deliveryId for
transport debugging.
await database.transaction(async (tx) => {
const inserted = await tx.claimWebhook(message.occurrenceId);
if (!inserted) return;
await applyBusinessEvent(tx, message);
});
return new Response(null, { status: 204 });Useful secondary constraints include:
(eventId, bookingRef)for a completed order;(eventId, holdId, event)for hold lifecycle;- your own immutable order or tenant reference.
Do not mark an occurrence processed before the business transaction succeeds.
Do not return 2xx for a failure you expect SeatLayer to retry.
Delivery ledger
The API stores the newest attempts for each subscription, including:
- HTTP status (
0when no response arrived); - attempt and maximum attempts;
willRetry;- occurrence id and transport id;
- the sent payload;
- a truncated non-
2xxresponse body; or - a transport error message.
The list defaults to 20, caps at 50 per page, and supports ok/failed
filtering. SeatLayer retains a bounded per-subscription history and applies a
30-day retention cleanup; export operational evidence you must retain longer.
Manual redelivery
Retrying from the dashboard or API sends the stored payload only to the selected
subscription. It preserves occurrenceId, creates a new attempt/signature, and
does not re-arm automatic retries.
Your receiver should return success for an occurrence that was already applied:
if (await wasProcessed(message.occurrenceId)) {
return new Response(null, { status: 204 });
}Failure-response hygiene
Non-2xx response text can appear in the delivery ledger for debugging.
Return a short safe error code. Never include secrets, stack traces, customer
records, request headers, or raw internal exception messages.
Production checklist
- Verify the raw body before JSON parsing.
- Compare signatures in constant time.
- Store and atomically claim
occurrenceId. - Return quickly; queue slow downstream work in your own system.
- Support events arriving out of order.
- Route by trusted workspace mapping and validate
livemode. - Monitor failed deliveries and seven-day uptime.
- Keep receiver errors short and non-sensitive.
- Test duplicate, delayed, malformed, expired-secret, and retry cases.
See manage subscriptions, webhook events, and signature verification.