Skip to content
Docs
Reference

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#

TypeWhenPayload
order.createdA new order was startedOrder
order.confirmedAn order was paid and confirmedOrder
order.cancelledAn order was cancelledOrder
order.refundedAn order was refundedOrder
customer.createdA new customer was createdCustomer
event.createdA new event was createdEvent
ticket.issuedTickets (passes) were issued for an orderTickets issued
ticket.redeemedA ticket was scanned and redeemedTicket redeemed
event.updatedAn event's content, images, status, occurrences, prices, venue or layout changed. At most one delivery per event every 30 secondsEvent updated
event.deletedAn event was deletedEvent deleted
listing.changedA channel listing was created, updated or removedListing changed
listing.status_changedWhat the public sees of a channel listing changed, including when a sale window opens or closes with no editListing status changed
venue.updatedA venue was editedVenue updated
customer.tag_grantedA customer was granted a tagCustomer tags
customer.tag_revokedA customer's tag was revoked or expiredCustomer 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.updated on data.sequence. It is strictly increasing per event. Drop a delivery whose sequence is 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.updated is coalesced. Changes to an event within 30 seconds are delivered once, with every area that changed in data.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:

causeMeans
scheduledA window opened or closed with no edit
manualThe listing rules were changed, and that changed the state now
window_editedA window was moved, and that moved the state
event_statusThe event entered or left a live status, or was deleted
listed, unlistedThe 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", "...": "..." }
}
HeaderValue
Content-Typeapplication/json
User-AgentTicketlayer-Webhooks/1.0
Ticketlayer-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Ticketlayer-Event-TypeThe event type, same as type in the body
Ticketlayer-Event-IdThe id of the source event; stable across retries of the same event
Ticketlayer-Delivery-IdThe 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.