Skip to content

Holders and customers

Read and update the customer behind a season holder reference, list their seasons and tickets, give seats back, renew them, and find customers by email.

Updated View as Markdown

A holder reference is your own name for a season ticket holder: the holderRef your imports and bookings already carry. A season bought through SeatLayer checkout uses its order id as the reference.

A customer is the record SeatLayer keeps for a holder, one per email and mode (cus_…). These routes connect the two, with your secret key. The key’s mode decides which customers you see: a test key never reaches a live customer.

How a holder gets a customer

PATCH /v1/holders/:holderRef links a holder to a customer at once, and makes the customer if it does not exist. Until then the holder is pending, and the reads on this page still answer for them.

An import row with holderEmail, and “Bring holders from a previous season”, keep the email on the holder, so PATCH needs no email for them. Only rows the import committed keep one, and an import never changes a holder that already has a link: to change a holder’s email, use PATCH /v1/holders/:holderRef.

Retrieve a holder

GET /v1/holders/legacy-cara
Authorization: Bearer sk_live_…
200json
{
  "holder": {
    "holderRef": "legacy-cara",
    "customerId": "cus_5f0c2e9a7b1d4c3e8f6a2b10",
    "email": "cara@example.org",
    "name": "Cara Holder",
    "phone": null,
    "status": "active"
  }
}

status is pending while the holder has no customer; customerId is then null. email and name are the customer’s own, else the ones you set, else the ones a season import gave the holder. A reference SeatLayer has never seen answers 404 holder_not_found.

Update a holder

PATCH /v1/holders/legacy-cara
Authorization: Bearer sk_live_…
Idempotency-Key: crm-update-8812

{ "email": "cara.new@example.org", "phone": "+44 7700 900123" }

Your server knows its holder, so a new email applies at once and sends customer.email_changed. If another customer of yours already has the email, the answer is 409 email_in_use. Nothing bought with the new address joins the customer, so a mistyped address shows a stranger nothing. The box office’s walk-up placeholder is not an email and answers 422.

A holder with no customer gets one for the email you send, or for the email an import named. With neither, the answer is 422 email_required. Send null to clear name or phone; a name an import gave then shows again. The answer is the holder as GET reads it. Retry with the same Idempotency-Key to get the original answer back.

Seasons and tickets

GET /v1/holders/:holderRef/seasons lists every season the holder’s customer holds seats in:

200json
{
  "seasons": [{
    "seasonKey": "sea_a245…",
    "name": "2027 Season",
    "package": { "planActivationId": "spa_0b23…", "name": "Full season" },
    "seats": ["A-6", "A-7"],
    "status": "reserved",
    "dates": [],
    "renewalOffer": null
  }]
}
status Meaning
active Booked on at least one date you have not cancelled. dates lists each one.
reserved Imported: the seats are held for them, not yet offered or booked.
offered A renewal offer is open; renewalOffer has its deadline.
renewed Renewed.
released Their seats were let go.
cancelled Every date was cancelled.

A date given back reads cancelled once every seat of it is back, whoever gave it back (the holder, or you with POST /v1/seasons/:key/occurrences/:eventKey/returns). Its seats leave the season’s seats while other dates stay booked.

A date you cancelled reads date_cancelled. It stops counting towards active, and once every date is cancelled the season reads cancelled. A date you postponed reads postponed and still counts, with newStartsAt once it moves (null until then).

GET /v1/holders/:holderRef/tickets lists tickets SeatLayer issued for the holder’s orders, earliest date first, 50 a page (limit up to 100). Filter with seasonKey, and from / to in epoch milliseconds on the date’s start. Pass nextCursor back as cursor for the next page; it is null on the last.

GET /v1/holders/:holderRef/tickets/:ticketId adds qrUrl and scanUrl.

A season sold through your own checkout has no SeatLayer tickets, so its dates show under seasons and the ticket list is empty.

Send tickets to someone else

Customer accounts are off, so a recipient has no page to accept tickets on. POST /v1/holders/:holderRef/transfers answers 409 customer_accounts_off, and nothing is saved or sent. GET /v1/holders/:holderRef/transfers lists nothing, and the accept and cancel routes answer 404 transfer_not_found.

Give a seat back on one date

A holder who cannot come to one date can give that seat back through your server, so your club can sell it again. Each ticket is one seat on one date; the holder keeps every other seat and date, and the right to renew.

POST /v1/holders/legacy-cara/returns
Authorization: Bearer sk_live_…
Idempotency-Key: crm-return-0412

{ "ticketIds": ["tkt_3f1c…"] }
201json
{
  "return": {
    "returnId": "ret_8d02…",
    "state": "returned",
    "seasonKey": "sea_a245…",
    "tickets": [{ "ticketId": "tkt_3f1c…", "eventKey": "ev_41b2…", "label": "B-14", "state": "returned", "error": null }],
    "createdAt": 1793664000000
  }
}

The ticket is voided first, so its QR stops working online and the door list’s version changes, which makes a door phone mark it void. Then the seat goes back on sale for that date, like any free seat. No money moves: a refund for the date is a separate partial refund. You get season.ticket.voided with reason returned, and season.occurrence.returned with origin holder once the seat is back on sale. In the season’s Orders tab and orders.csv, a date whose seats were all given back shows as cancelled.

Each ticket, and the return as a whole, has a state:

state Meaning
returned The ticket is void and the seat is back on sale.
pending The ticket is void and the seat is still being released. It finishes on its own within the hour, or at once when you send the same tickets again.
failed The ticket is void, but the seat could not be given back. error says why: source_not_active means the booking ended meanwhile (cancelled or refunded), so its seats went back with it.

Sending the same tickets again answers the same return as it stands now (200). If a return was cut off part way, sending them again finishes the tickets it had not reached (201).

Refusal When
404 ticket_not_found A ticket is not this holder’s.
409 ticket_checked_in A ticket was used at the door.
409 ticket_not_returnable A ticket is void or not held now, or they span seasons.
409 own_checkout The season sells through your own checkout; SeatLayer holds no seat to give back.
409 return_cutoff_passed A date starts within the season’s return cut-off (returnCutoffHours, set with PATCH /v1/seasons/:key, 0–720; null means until the date starts).
409 performance_cancelled The date was cancelled: its money goes back through the date’s refund job.

Renew a holder’s seats

When SeatLayer takes the payments for a season, your server can renew a holder with no SeatLayer page in between. Take the offerId from the holder’s season (renewalOffer in Seasons and tickets).

POST /v1/holders/legacy-cara/renewal-offers/sro_51e0…/checkout
Authorization: Bearer sk_live_…
Idempotency-Key: crm-renew-cara-2027

{ "returnUrl": "https://tickets.yourclub.example/renewed" }
201json
{
  "orderId": "ord_ea11…",
  "totalMinor": 18000,
  "currency": "GBP",
  "expiresAt": 1793664900000,
  "payment": { "url": "https://checkout.provider.example/…" }
}

Send the holder to payment.url. Paying renews the offer on every date, and you get season.order.paid and season.renewal.renewed. The request, the answer and the rules are those of the season’s renewal checkout, where the quote is too.

POST /v1/holders/legacy-cara/renewal-offers/sro_51e0…/decline
Authorization: Bearer sk_live_…

Declines for the holder and releases their seats at once; you get season.renewal.declined. An optional { "reason": "Moving away" } (1–200 characters) is kept on the offer as declineReason. Declining again answers the same offer, with the first reason.

Both answer 404 offer_not_found for an offer that is not this holder’s. The offers of every reference of the holder’s customer count, as in GET /v1/holders/:holderRef/seasons.

Find customers

GET /v1/customers lists your customers in the key’s mode, in customer id order (ids are random, so this is a stable order for paging, not by age), each with its holderRefs. Filter with email or holderRef; page with cursor and limit as above.

200json
{
  "customers": [{
    "id": "cus_5f0c2e9a7b1d4c3e8f6a2b10",
    "email": "cara@example.org",
    "name": "Cara Holder",
    "phone": null,
    "status": "active",
    "mode": "live",
    "createdAt": 1793664000000,
    "holderRefs": ["legacy-cara"]
  }],
  "nextCursor": null
}

These routes share the season API’s budget of 600 calls a minute per key. No server SDK wraps them; call the REST routes from any language.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close