---
title: "Delivery and retries"
description: "Build an idempotent webhook receiver for asynchronous, scoped, bounded-attempt delivery."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.seatlayer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Delivery and retries

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 `2xx` acknowledges the delivery.
- Non-`2xx` responses 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.

```ts title="server/idempotent-receiver.ts"
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 (`0` when no response arrived);
- attempt and maximum attempts;
- `willRetry`;
- occurrence id and transport id;
- the sent payload;
- a truncated non-`2xx` response 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:

```ts
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](/webhooks/manage-subscriptions),
[webhook events](/webhooks/events), and
[signature verification](/webhooks/signatures).

Source: https://docs.seatlayer.io/webhooks/delivery-and-retries/index.mdx
