Every delivery is an HTTPS POST with one stable business occurrence id and
one transport-attempt id.
{
"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:
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.createdwithreplaced: trueand the complete new held set. - Labels removed during replacement also emit
seat.released. - Expiry emits one
hold.expiredper hold and a release occurrence for freed labels. - Manual hold release, unblock, scheduled block release, and cancellation can
all produce
seat.released. hold.extendedcarries 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
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 and signature verification.