Skip to content

Webhook events

Understand the signed delivery envelope, occurrence identity, routing metadata, and all public event payloads.

Updated View as Markdown

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
season.amendment.applied Reschedule or explicit occurrence exception recorded amendmentId, eventKey, kind, classification
season.occurrence.returned (candidate) One booked Season occurrence is returned to ordinary Event inventory actionId, returnActionId, eventKey, planActivationId, bookingRef, source, labels
season.occurrence.reclaimed (candidate) A still-free occurrence return is restored to its Season holder actionId, returnActionId, eventKey, planActivationId, bookingRef, source, labels
season.operation.partial_terminal Season operation reached visible mixed terminal outcomes operation identity and occurrence outcomes

Fixed Renewable Seasons also emit structure, Plan, sales, hold, booking, and renewal lifecycle names under the season.* namespace. Subscribe using the dashboard event picker or the exact names in the Seasons API contract. Each delivery retains one stable occurrenceId across automatic and manual replay. The two occurrence inventory events above belong to the unpublished REST/Node candidate and are not a claim about the deployed baseline.

Occurrence return and reclaim remain separate candidate operations: they emit season.occurrence.returned or season.occurrence.reclaimed through the Season coordinator and project realtime inventory, not the standard Event seat.released or seat.booked occurrences described in the caution above.

event.soldout is guarded to emit once per event. A cancellation followed by a new ordinary booking of the freed inventory does not create a second sold-out occurrence. This webhook wording does not imply a first-class resale/listing state; the current Platform SDK has none.

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.

Sale attribution on seat.booked

Items on a seat.booked payload additionally carry which sales channel made the sale, recorded at the moment of booking and never rewritten afterwards.

interface BookedLineItem extends WebhookLineItem {
  /** The channel that sold this unit. `null` means Public sale. */
  channelId: string | null;
  /** That channel's stable external reference, when it has one. */
  channelExternalRef: string | null;
  /** How the buyer was authorized when the unit was captured. */
  accessSource: "public" | "promoter" | "partner" | "hosted_link" | "staff_override";
  /** Present only when the buyer's grant carried a partner reference. */
  partnerRef?: string;
}
{
  "label": "A-12",
  "channelId": "chn_7f2a",
  "channelExternalRef": "travel-agency-a",
  "accessSource": "partner",
  "partnerRef": "travel-agency-a"
}

Four things worth knowing before you write the handler:

  • channelId: null is a value, not a gap. It means the seat was sold on Public sale. The field is always present on a booked item, so null and “an older delivery that predates this” stay distinguishable.
  • Attribution is per item, not per delivery. One bookingRef can cover units captured under different channels and different access sources. Do not read items[0] and assume the rest match.
  • It never changes. Moving or archiving a channel later does not rewrite the attribution on a sale that already happened, which is what makes these fields safe reconciliation keys.
  • channelExternalRef is the key you can match on. A SeatLayer channel id means nothing in your partner’s system; the external reference is the one you set yourself.

Reconciliation keys

For a booking that came through a private channel, reconcile on:

  • the SeatLayer bookingRef;
  • your own order reference;
  • the partner’s order reference, where you captured one;
  • channelExternalRef — stable across renames;
  • the occurrenceId and your idempotency key, for replay safety.

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

server/webhooks.tsts
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.

An Event booking response or exact Booking History record, and a terminal Performance Group or Season booking poll, remain the checkout command outcome. Use these deliveries to reconcile that outcome, not as permission to charge or fulfil a second time.

Continue to manage subscriptions, delivery and retries, and signature verification.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close