Offers
What a ticket costs on a sales channel and whether it can be bought: the face value, each fee and where it came from, the total, availability with a reason, a capacity bucket, the order limits and a quote. One engine, the one checkout charges through.
An offer is one thing a buyer can pick on one date: a seating category and a ticket type, on an occurrence, on a sales channel. "Stalls, Adult, on Friday's performance, on the website" is an offer. The Backstage API answers every question a page asks about offers from one route:
GET /v1/events/{eventId}/offersIt returns each offer with its price, fees included, and whether this caller
can buy it now, and why not when they cannot. Carts and checkout price through
the same engine, so the total an offer shows is what checkout charges for
that ticket, and a quote for two tickets is what checkout charges for a cart
holding those two. Show a price from here, not from a price scheme: a scheme
holds face values, and a buyer pays fees on top.
The operation reference is Get event offers,
and @ticketlayer/backstage has it as client.events(eventId).offers.get().
The channel is required#
Prices, fees and sale windows belong to a sales channel. The same ticket can cost more on a partner's site than on your own, a channel's fee profile adds its own booking fee, and a presale can open on one channel before another. So an offer only has an answer on a channel, and the route requires one:
- send
X-Ticketlayer-Channelwith the channel's slug, or - use a credential that names its own channel, in which case the header, if sent, must agree with it.
Without a channel the answer is 400 CHANNEL_REQUIRED.
An event the channel does not show to this caller is 404, the same as reading
it on that channel.
The caller needs events.read on the event's account. An organisation API
key (tlak_) with the events read scope or a staff token will do.
A worked example#
The event "The Tempest" has one performance, one category (Stalls) and two
ticket types. The price scheme says Adult is 20.00 and Concession 15.00, and
the website channel has its own Adult price of 22.00. The channel's fee
profile adds a 10% service fee per ticket, and the account charges a 1.25
order fee once per order.
curl "https://api.ticketlayer.com/v1/events/evt_8f2kQ/offers?quantity=2" \
-H "Authorization: Bearer tlak_..." \
-H "X-Ticketlayer-Org: your-org-slug" \
-H "X-Ticketlayer-Channel: website"const offers = await backstage.events('evt_8f2kQ').offers.get({ quantity: '2' });The Adult offer comes back like this (one offer shown, JSend envelope omitted):
{
"event": { "id": "evt_8f2kQ", "name": "The Tempest", "timezone": "Europe/London" },
"channel": { "id": "sch_web", "slug": "website", "name": "Website" },
"context": { "at": "2026-10-01T09:00:00.000Z", "view": "buyer", "customerId": null, "code": null, "codeApplied": false, "quantity": 2, "cartId": null },
"summary": { "state": "on_sale", "currency": "GBP", "from": 1775, "to": 2545 },
"limits": { "minPerOrder": 1, "maxPerOrder": 10, "maxOrderValue": 1000000 },
"occurrences": [
{
"id": "occ_1",
"startsAt": "2026-10-09T18:30:00.000Z",
"endsAt": "2026-10-09T21:00:00.000Z",
"timezone": "Europe/London",
"status": "on_sale",
"availability": { "state": "on_sale", "reason": null, "opensAt": null, "closesAt": "2026-10-09T18:00:00.000Z" },
"offers": [
{
"categoryId": "cat_stalls",
"categoryName": "Stalls",
"ticketTypeId": "tt_adult",
"ticketTypeName": "Adult",
"description": null,
"price": {
"currency": "GBP",
"faceValue": 2200,
"discounts": [],
"fees": [
{ "code": "service", "label": "Service fee", "basis": "per_ticket", "rate": 0.1, "amount": 220, "source": { "layer": "channel_profile", "id": "fpr_web" } },
{ "code": "order", "label": "Order fee", "basis": "per_order", "rate": null, "amount": 125, "source": { "layer": "account_config", "id": "acc_main" } }
],
"total": 2545,
"source": { "layer": "channel", "schemeId": "ps_main", "level": 0, "inForceSchemeId": "ps_main" }
},
"unlock": null,
"availability": { "state": "on_sale", "reason": null, "opensAt": null, "closesAt": "2026-10-09T18:00:00.000Z" },
"remaining": { "bucket": "limited", "exact": null },
"limits": { "minPerOrder": 1, "maxPerOrder": 10, "maxPerOrderRemaining": 10 },
"quote": { "quantity": 2, "faceValue": 4400, "discounts": 0, "fees": 565, "total": 4965 }
}
]
}
]
}Reading it:
faceValueis 2200, the website's own Adult price.price.source.layerischannelbecause the channel has a price for this cell; the Concession offer, which has none, saysdefaultand prices at the scheme's 1500.levelis 0 because the price is in the scheme in force; 1 would mean it was inherited from that scheme's parent.feeslists every fee on buying one ticket. The service fee is 10% of 2200 (rate0.1,amount220). The order fee isper_order: it is charged once whatever the quantity, and it is in the single-tickettotalbecause one ticket bought alone pays it. Each fee'ssourcenames the configuration that charged it:organisation_config,account_config,channel_profile,profile_ruleorcode_waiver.totalis 2545:faceValue - discounts + fees, what one ticket costs. Money is always integer minor units incurrency.quotepricesquantity(2 here): face value 4400, fees 440 per ticket plus the 125 order fee once, 565, for 4965. A quote is present only when the offer is on sale.summaryis the event on this channel in one line: the best state any offer is in, and the cheapest and dearesttotalamong the offers this buyer can pay for now (or, when none is on sale, among those that open later). Concession's total is 1500 + 150 + 125 = 1775.
Availability#
Every occurrence and every offer carries availability: a state, a
reason when it is not on sale, and the times that matter to it.
| State | Means | Carries |
|---|---|---|
on_sale | This caller can buy it now | closesAt: when the sale they are under closes, or null when it does not |
not_yet_on_sale | A sale window opens later | opensAt: when it opens for this caller |
sold_out | Nothing left | |
off_sale | Not on sale, and no window opens | closesAt: when the sale closed, where one did |
hidden | The channel does not show it to this caller | only in the staff view (below) |
locked | Reserved in the schema | see Offers that need a tag |
The state of an occurrence is the same for every offer on it (the event is
not live, the performance is cancelled, the sale window has not opened); an
offer can then be worse than its occurrence, for example sold_out while
other categories on the same date are still on_sale.
A buyer is told one of four coarse reasons, which are safe to show on a page:
| Reason | State | Tell the buyer |
|---|---|---|
SALE_NOT_OPEN | not_yet_on_sale | On sale from opensAt |
SOLD_OUT | sold_out | Sold out |
SALE_CLOSED | off_sale | Sales have closed |
NOT_AVAILABLE | off_sale | Not available |
With view=staff, the reason is the fine one instead, which is what a box
office or a support tool needs to fix the event: EVENT_NOT_LIVE,
OCCURRENCE_DRAFT, OCCURRENCE_CANCELLED, OCCURRENCE_COMPLETED,
NO_PRICE_SCHEME, PRICE_SCHEME_NOT_ACTIVE, NO_PRICE_FOR_TYPE,
NO_INVENTORY, OCCURRENCE_SOLD_OUT, NO_CAPACITY, NOT_LISTED and
NOT_VISIBLE, alongside SALE_NOT_OPEN and SALE_CLOSED.
Remaining capacity#
remaining.bucket says how much of the offer's category is left, in words a
page can show without publishing a count:
| Bucket | Means |
|---|---|
available | More than 20% of the category's capacity, and more than 10 tickets |
limited | 20% of the capacity or less |
few | 10 tickets or fewer |
none | Nothing left |
bucket is null when the category has no inventory. remaining.exact, the
count itself, is filled in only with view=staff for a caller holding
inventory.read; everywhere else it is null.
Limits#
limits on the response are the order limits checkout enforces:
minPerOrder, maxPerOrder (10 unless the organisation's order
configuration sets another) and maxOrderValue, in minor units. Each offer
repeats minPerOrder and maxPerOrder in its own limits. Pass
cartId, an open cart on the same channel, and each offer's
maxPerOrderRemaining is maxPerOrder less what that cart already holds, so a
quantity picker can stop at the right number. It is 0 on a sold-out offer.
The buyer view and the staff view#
view=buyer is the default: what a member of the public on the channel is
told. It lists the occurrences a buyer on the channel sees (on sale or sold
out), the coarse reasons, the bucket without a count, and no unlock.
view=staff lists every occurrence, with the fine reasons and, for a caller
holding inventory.read, remaining.exact. It also prices an occurrence
whose scheme is not active yet, so the box office can see what a draft says
and why it is off sale. A listing that is switched on but not visible right
now (a link-only listing outside its window, say) is 404 in the buyer view;
in the staff view, for a caller holding events.list_hidden on the channel,
it is returned with the state hidden and the reason NOT_VISIBLE.
Pricing for one customer, a code, an instant#
customerIdprices for that customer: their customer tags are read server-side and admit them to what those tags gate. It needscustomers.readon the customer.codepreviews what a presale code would unlock. It is never redeemed and no use is counted;context.codeAppliedsays whether it unlocks anything now.atevaluates at another instant (ISO 8601), for example to check what a page will show when a window opens.occurrenceIdnarrows the response to one date.
Offers that need a tag#
A ticket type can require a customer tag, for example a members' price. An
offer of that ticket type is returned only to a caller who holds the tag,
because they are the customer named by customerId or because the code
previewed grants it; unlock.source then names the tag in the staff view.
For anyone else the offer is left out of the response, so a public page never
shows a price its reader cannot pay. The schema also declares a locked state
with the buyer reason TAG_REQUIRED; the engine does not return it, and a
gated offer is always left out rather than returned as locked.
Offers and the event read#
For a card or a list, where one line per event is enough, read the events with
include=prices on a channel instead of calling offers once per event. Each
event's prices is the same summary the engine computes: from and to
(fees included, feesIncluded: true), currency, state and sellable. See
Syncing events.