---
title: "Webhook events"
description: "Understand the signed delivery envelope, occurrence identity, routing metadata, and all public event payloads."
---

> 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.

# Webhook events

Every delivery is an HTTPS `POST` with one stable business occurrence id and
one transport-attempt id.

```json
{
  "deliveryId": "d_91c3",
  "occurrenceId": "evtocc_8a2f",
  "event": "seat.booked",
  "at": 1761436800000,
  "payload": {
    "workspaceId": "ws_7e2d",
    "eventId": "ev_9f3a",
    "labels": ["A-1"],
    "bookingRef": "order_42",
    "livemode": true
  }
}
```

| Header | Value |
|---|---|
| `Content-Type` | `application/json` |
| `X-SeatLayer-Event` | Event name |
| `X-SeatLayer-Signature` | `sha256=` plus the raw-body HMAC |

Verify the signature before parsing or acting on the payload.

## Identity and routing fields

| Field | Meaning |
|---|---|
| `occurrenceId` | Stable business occurrence; idempotency key across retries and subscriptions |
| `deliveryId` | One HTTP delivery attempt |
| `at` | Occurrence time in epoch milliseconds |
| `payload.workspaceId` | Immutable workspace boundary |
| `payload.externalRef` | Optional host reconciliation tag |
| `payload.environment` | Optional routing environment inherited from event creation |
| `payload.livemode` | `false` for test events, otherwise `true` |

Use `workspaceId` plus your stored mapping to route tenants. Never use
`externalRef` alone as authorization.

## Event catalog

| Event | Fires when | Main payload fields |
|---|---|---|
| `event.created` | Event materialization succeeds | `eventId`, `name`, `chartId`, `workspaceId`, `at` |
| `hold.created` | A hold is created or replaced | `eventId`, `labels`, `holdId`, `expiresAt`, `items`, `replaced?` |
| `hold.extended` | Active hold expiry moves forward | `eventId`, `holdId`, `expiresAt`, `extends` |
| `hold.expired` | Hold reaches expiry | `eventId`, `labels`, `holdId`, `items` |
| `seat.booked` | Buyer/server/POS booking succeeds | `eventId`, `labels`, `bookingRef`, `items`, `holdId?`, `at` |
| `seat.released` | Held, blocked, or booked inventory returns to free | `eventId`, `labels`, `holdId?`, `items?` |
| `seat.blocked` | Organizer blocks free inventory | `eventId`, `labels` |
| `event.soldout` | Last free inventory is booked for the first time | `eventId`, `at` |

`event.soldout` is guarded to emit once per event. A cancellation and later
resale does not create a second sold-out occurrence.

## Line items

Where present, `items` are server-generated inventory snapshots:

```ts
interface WebhookLineItem {
  label: string;
  objectId: string;
  objectType: "seat" | "booth" | "ga" | "table";
  categoryKey: string;
  tierId: string | null;
  unitPrice: number;
  currency: string;
  quantity: number;
}
```

Use them for order reconciliation and customer messaging. Your payment system
remains authoritative for charges, captures, refunds, taxes, and settlement.

## Replacement and release nuances

- Replacing a hold emits `hold.created` with `replaced: true` and the complete
  new held set.
- Labels removed during replacement also emit `seat.released`.
- Expiry emits one `hold.expired` per hold and a release occurrence for freed
  labels.
- Manual hold release, unblock, scheduled block release, and cancellation can
  all produce `seat.released`.
- `hold.extended` carries no labels because inventory identity did not change.

Design handlers around the event name and ids rather than inferring the cause
only from a seat status.

## Minimal handler

```ts title="server/webhooks.ts"
export async function handleSeatLayerWebhook(request: Request) {
  const rawBody = await request.text();
  verifySeatLayerSignature(
    rawBody,
    request.headers.get("x-seatlayer-signature"),
  );

  const message = JSON.parse(rawBody);

  if (await wasProcessed(message.occurrenceId)) {
    return new Response(null, { status: 204 });
  }

  switch (message.event) {
    case "seat.booked":
      await reconcileBooking(message.payload);
      break;
    case "seat.released":
      await reconcileRelease(message.payload);
      break;
    default:
      await recordUnhandledEvent(message);
  }

  await markProcessed(message.occurrenceId);
  return new Response(null, { status: 204 });
}
```

Persist the business mutation and processed occurrence id atomically when
possible. Return `2xx` for a known duplicate.

Continue to [delivery and retries](/webhooks/delivery-and-retries) and
[signature verification](/webhooks/signatures).

Source: https://docs.seatlayer.io/webhooks/events/index.mdx
