Webhooks
Signed POSTs when orders, customers, events, listings, venues, customer tags and tickets change: the fifteen event types, their payloads, the envelope, deduplication, ordering with sequence, and retries.
Register a URL and Ticketlayer POSTs a signed JSON envelope to it whenever one of the events below happens in your organisation. Payloads are constructed field by field from a fixed, versioned view (never a raw database row), so what is documented here is exactly what arrives.
Event types#
| Type | When | Payload |
|---|---|---|
order.created | A new order was started | Order |
order.confirmed | An order was paid and confirmed | Order |
order.cancelled | An order was cancelled | Order |
order.refunded | An order was refunded | Order |
customer.created | A new customer was created | Customer |
event.created | A new event was created | Event |
ticket.issued | Tickets (passes) were issued for an order | Tickets issued |
ticket.redeemed | A ticket was scanned and redeemed | Ticket redeemed |
event.updated | An event's content, images, status, occurrences, prices, venue or layout changed. At most one delivery per event every 30 seconds | Event updated |
event.deleted | An event was deleted | Event deleted |
listing.changed | A channel listing was created, updated or removed | Listing changed |
listing.status_changed | What the public sees of a channel listing changed, including when a sale window opens or closes with no edit | Listing status changed |
venue.updated | A venue was edited | Venue updated |
customer.tag_granted | A customer was granted a tag | Customer tags |
customer.tag_revoked | A customer's tag was revoked or expired | Customer tags |
GET /v1/webhook-event-types returns this list with descriptions, which is
what the Backstage subscription UI shows.
Payloads#
Money is in integer minor units with an ISO 4217 currency ("totalAmount": 2500, "currency": "GBP" is 25.00 GBP). Instants are ISO 8601 strings. Every
nullable field is present as null rather than omitted.
Order (order.*)
{
"id": "ord_01J...",
"orderNumber": "TL-10432",
"status": "confirmed",
"currency": "GBP",
"totalAmount": 5400,
"subtotalAmount": 5000,
"customerId": "cus_01J...",
"accountId": "acc_01J...",
"createdAt": "2026-09-13T19:04:11.000Z",
"lineItemCount": 2
}Customer (customer.created)
{
"id": "cus_01J...",
"email": "ada@example.com",
"firstName": "Ada",
"lastName": "Lovelace",
"accountId": "acc_01J...",
"createdAt": "2026-09-13T19:04:11.000Z"
}Event (event.created)
{
"id": "evt_01J...",
"name": "Jazz Night",
"status": "draft",
"accountId": "acc_01J...",
"venueId": "vnu_01J...",
"timezone": "Europe/London",
"createdAt": "2026-09-13T19:04:11.000Z"
}Tickets issued (ticket.issued)
{
"orderId": "ord_01J...",
"accountId": "acc_01J...",
"passCount": 2,
"passes": [
{ "id": "pas_01J...", "ticketTypeId": "tkt_01J...", "occurrenceId": "occ_01J...", "status": "issued" },
{ "id": "pas_01K...", "ticketTypeId": "tkt_01J...", "occurrenceId": "occ_01J...", "status": "issued" }
]
}Ticket redeemed (ticket.redeemed)
{
"redemptionId": "red_01J...",
"passId": "pas_01J...",
"entitlementId": "ent_01J...",
"accountId": "acc_01J..."
}Change notifications#
event.updated, event.deleted, listing.changed, listing.status_changed
and venue.updated are thin: they say what changed and carry the ids to
re-fetch, never the content itself. An event's description, dates or prices
are read from the API when the delivery arrives, so a receiver always stores
the current version, whatever order deliveries arrive in and however many
were retried. Syncing events
shows a receiver built on them.
- Deduplicate on
Ticketlayer-Event-Id. Deliveries are at least once, and the id is stable across retries and across endpoints. - Order
event.updatedondata.sequence. It is strictly increasing per event. Drop a delivery whosesequenceis lower than one already processed for that event: a later delivery has already been acted on, and the re-fetch it caused was newer. event.updatedis coalesced. Changes to an event within 30 seconds are delivered once, with every area that changed indata.changes, so editing ten dates is one delivery. Two deliveries for one event are never less than 30 seconds apart.
Event updated#
event.updated, to the event's account. changes is the set of areas that
changed since the previous delivery: content, images, status (the event
status and its listing rules), occurrences, prices, venue and layout.
It is a hint for what to re-fetch; the event itself is read from
GET /v1/events/{id}. updatedAt is the event's contentUpdatedAt.
sequence is contentUpdatedAt in epoch milliseconds, or one more than the
previous delivery's when that is not greater.
{
"id": "evt_01J...",
"organisationId": "org_01J...",
"name": "Jazz Night",
"status": "on_sale",
"accountId": "acc_01J...",
"venueId": "vnu_01J...",
"timezone": "Europe/London",
"createdAt": "2026-09-13T19:04:11.000Z",
"updatedAt": "2026-09-20T10:15:42.120Z",
"changes": ["occurrences", "prices"],
"sequence": 1789899342120
}Event deleted#
event.deleted, to the event's account. An event.updated still waiting in
its 30 second window when the event is deleted is not sent.
{
"id": "evt_01J...",
"organisationId": "org_01J...",
"accountId": "acc_01J...",
"deletedAt": "2026-09-20T10:15:42.000Z"
}Listing changed#
listing.changed, to the channel's account and the event's account (once
when they are the same): a listing's settings were saved.
change is created, updated or removed, and every save sends one, even
a save that changes nothing a buyer sees. state is what the public sees of
the listing after the write: discoverable (in the channel's list),
visible (can be opened), sellable, the next moment it becomes sellable
(opensAt) and the end of the window it sells under (closesAt).
nextTransitionAt is the next window boundary at which that state can change
with no edit, or null; a receiver can schedule its own re-fetch for it.
{
"eventId": "evt_01J...",
"listingId": "lst_01J...",
"channel": { "id": "sch_01J...", "slug": "website" },
"organisationId": "org_01J...",
"accountId": "acc_01J...",
"change": "updated",
"state": {
"discoverable": true,
"visible": true,
"sellable": false,
"opensAt": "2026-10-01T09:00:00.000Z",
"closesAt": null
},
"nextTransitionAt": "2026-10-01T09:00:00.000Z",
"occurredAt": "2026-09-20T10:15:42.000Z"
}Listing status changed#
listing.status_changed, to the same accounts as listing.changed: discoverable,
visible or sellable changed, and only when one did. It is sent when a
sale window opens or closes with no edit, so a page can switch to "on sale"
without polling. previous is the state before; state and
nextTransitionAt are as in listing.changed. cause says why:
cause | Means |
|---|---|
scheduled | A window opened or closed with no edit |
manual | The listing rules were changed, and that changed the state now |
window_edited | A window was moved, and that moved the state |
event_status | The event entered or left a live status, or was deleted |
listed, unlisted | The listing was created or removed |
{
"eventId": "evt_01J...",
"listingId": "lst_01J...",
"channel": { "id": "sch_01J...", "slug": "website" },
"organisationId": "org_01J...",
"accountId": "acc_01J...",
"cause": "scheduled",
"previous": { "discoverable": true, "visible": true, "sellable": false },
"state": { "discoverable": true, "visible": true, "sellable": true, "opensAt": null, "closesAt": null },
"nextTransitionAt": null,
"occurredAt": "2026-10-01T09:00:00.000Z"
}Venue updated#
venue.updated, to the venue's account. changes names the venue fields
that changed, by their API names; a save that changes none of them sends
nothing. The events at the venue that are on sale also receive
event.updated with venue in changes.
{
"id": "vnu_01J...",
"organisationId": "org_01J...",
"accountId": "acc_01J...",
"updatedAt": "2026-09-20T10:15:42.000Z",
"changes": ["name", "streetAddress"]
}Customer tags#
customer.tag_granted and customer.tag_revoked say a customer gained or
lost a tag. They go to the customer's account;
a customer the organisation's accounts share has accountId: null and is
delivered to every account in the organisation with an endpoint.
A grant is announced granted once and, if it ends, revoked once.
customer.tag_revoked carries reason: revoked when staff or an API key
took the tag away, expired when its expiresAt passed, in which case
occurredAt is the expiry instant. source is where the grant came from:
manual, membership, import or presale (a presale code redemption).
{
"customerId": "cus_01J...",
"tag": "member",
"customerTagId": "cent_01J...",
"source": "membership",
"expiresAt": "2027-08-31T23:59:59.000Z",
"organisationId": "org_01J...",
"accountId": "acc_01J...",
"reason": "expired",
"occurredAt": "2027-08-31T23:59:59.000Z"
}customer.tag_granted has the same fields without reason, and its
occurredAt is when the tag was granted.
The delivery#
Each delivery is one POST with a JSON body:
{
"id": "whd_01J...",
"type": "order.confirmed",
"version": "2026-06-13",
"createdAt": "2026-09-13T19:04:12.000Z",
"data": { "id": "ord_01J...", "orderNumber": "TL-10432", "...": "..." }
}| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | Ticketlayer-Webhooks/1.0 |
Ticketlayer-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Ticketlayer-Event-Type | The event type, same as type in the body |
Ticketlayer-Event-Id | The id of the source event; stable across retries of the same event |
Ticketlayer-Delivery-Id | The id of this delivery attempt series, same as id in the body |
Respond with any 2xx within 15 seconds. Anything else, or a timeout, is
retried with backoff; the delivery is marked failed when retries are
exhausted. Deliveries and their responses are listed at
GET /v1/webhook-deliveries and in Backstage.
Use Ticketlayer-Event-Id to deduplicate: a retry carries the same value,
and so does the same event delivered to another of your endpoints.
Payload versions#
Webhook payloads are a public contract. Each endpoint pins the payload version
current when it was created (2026-06-13 today) and keeps receiving that
shape: when a payload changes in a breaking way a new dated version is added
with a transform back to the previous one, and deliveries are transformed down
to the endpoint's pinned version before sending. The version is stamped in
every envelope as version.
Receiving them is a separate job: see Receive webhooks for registering an endpoint, verifying the signature and testing against a tunnel.