Purchase Flow
Six steps (one optional) to browse, reserve, and purchase tickets via the TTG API, then what to do after checkout.
This page documents direct v3 API integration. For TTG-hosted checkout, see Whitelabel Checkout. For agent/reseller flows on the v1/v4 stack, see Powered by TodayTix Agent APIs.
1. List Events
Returns a paginated list of events the authenticated retailer has access to. The locationId query parameter is required — it filters events by market (e.g., New York, London). Add query to narrow the list to events matching a search term.
Use limit and offset for pagination. Each event includes id, name, startDate, endDate, and location.
2. Browse Occurrences
Returns the event’s occurrences — its dates and times — with pricing summaries, sold-out dates included, 50 to a page unless you set limit (up to 1000): page with offset until you have pagination.total (see Pagination). The exception: when the event or your retailer account is set to list only dates with something on sale (for example, to hide dates that are not announced yet), every date with nothing to buy is left out. Use availabilityLevel to drive the calendar UI: SOLD_OUT (nothing to buy on this date — fromPrice is also null), LOW (tickets are running out), or null (no signal — display normally). Treat any other value as no signal. A null fromPrice means nothing is on sale for this occurrence (sold out, off sale, or not yet on sale); it can’t be bought.
The discount fields represent the best available discount across all price options: maxDiscountAmount whenever there is a discount, and maxDiscountPercentage too on events that show savings as a percentage. They may come from a different price option than fromPrice.
3. Browse Inventory
Returns purchasable items, 50 to a page unless you set limit (up to 1000). A large venue has more seats than that, so page with offset until you have pagination.total before you draw a seat map. Use inventoryVariant._type to determine the type:
Keep the number of tickets in one order within the event’s orderLimit (min to max), which the occurrence also carries. A GA item with quantityAvailable: 0 is a sold-out area kept on display: it has no priceOptions, so show it as unavailable and don’t add it to a cart.
For full details on each type, attributes, and pricing, see Inventory Types.
Party size: send ?quantity= with the number of tickets the customer wants. An assigned seat with selectionGroup can be picked: add every seat in the group to the cart together. Any other seat is unavailable at that party size. See Seats for a party.
Pricing: sellPrice is the all-in price customers pay (fees and tax included). listPrice is the price before a discount — display it as strikethrough. Price Options says when each is sent.
Example response (trimmed):
4. Create Cart
Reserves inventory and starts a hold timer. All items must be from the same occurrence. A selection that would leave a single seat on its own returns 409 ORPHANED_SEATS; ask the customer to choose different seats. The cart is created in a currency — the event’s base currency unless the body names a currency — and carries it on the response. To pay in another currency, PATCH the cart before checkout (see Currency).
A cart is accepted with fewer tickets than the event’s orderLimit.min, but placing the order for it returns 400 EVENT_MINIMUM_NOT_MET, so keep the selector within orderLimit (see Browse Inventory) rather than letting the customer reach checkout with too few.
When the customer picks seats again, create a new cart and send the old cart’s ID as previousCartId. Once the new cart is created, the old hold is released, so the customer doesn’t hold two sets of seats. The release never fails the create: an old cart that is gone or already an order is left alone. When the request carries a customer access token, the other carts created with that same token are also released before the new one is held, whether or not you send previousCartId. This release is best-effort on the same terms: it never fails the create, a cart whose order is being placed is left alone, and carts the customer made with another token are not touched.
The response includes expiresAt (UTC) — display a countdown. When the cart expires, inventory is released and GET /api/v3/carts/{cartId} returns 410 CART_EXPIRED (see Cart state). Create a new cart to continue.
To release held inventory early: DELETE /api/v3/carts/{cartId}. It returns 204, also for a cart already deleted or expired. A cart ID that doesn’t exist returns 404, and a cart that became an order returns 409 CART_ALREADY_ORDERED.
Vouchers
A voucher is a promo or access code the customer types in. Apply it in one of two ways:
- When you create the cart, send it as
voucherCodeonPOST /api/v3/carts. An access code that unlocks hidden seats must go here: applying it to a cart that already exists is too late to unlock them. Send the samevoucherCodeon the occurrences andinventory-itemsrequests, so the customer sees the dates and seats it unlocks. A code that can’t be used is ignored onPOST /carts, with no error: the cart is created without it. - On a cart that already exists, send it to
POST /api/v3/carts/{cartId}/vouchersas{ "code": "SUMMER20" }. It returns201with the cart repriced, or400 INVALID_VOUCHERwhen the code can’t be used. Some codes are limited to one use per customer: for a guest, add the customer’semailso the code can be checked.DELETE /api/v3/carts/{cartId}/vouchers/{code}removes it.
The cart’s voucher shows the code applied and its discount.
5. Add Extras (optional)
Some events offer extras — add-ons such as merchandise, drinks, or parking. The cart response lists them under options.extras, each in the same inventory-item shape as seats and GA (see Inventory Types). The list is empty when the event offers none.
Add them to the cart in one request, each with its inventory item ID, one of its price option IDs and a quantity. Here the customer adds one Adult and one Child Ice Cream:
Returns 201 with the updated cart: the seat (12500) and the two Ice Creams (4500 and 1500) make a total of 18500. Each extra appears in items[] with an Extra inventory variant, and in the cart total and receiptLines[]. The request is held as a unit: either every extra is added, or nothing changes and one error comes back. When one item is at fault (a malformed field, a price option the item’s occurrence cannot take, an occurrence the cart cannot take), the error’s param names it, for example items[1].priceOptionId. A refusal of the summed quantity — over a cap or the remaining availability — or by the venue’s system names no item. Two items naming the same extra and price option, or an item naming an extra already on the cart, merge into one line with the quantities summed.
Changing your mind
An extra already on the cart is addressed by the id of the line it came back as:
Both return 200 with the cart repriced. items[].id names the line, not the inventory — the same extra at two price options is two lines, each addressable on its own, so changing one leaves the other alone.
quantityis the number to end up holding, up or down from the number held now. It is at least1: to drop the item, sendDELETErather thanPATCHwith0.- Raising a quantity is subject to the same limits as adding: above
maxQuantityPerOrderreturns400, and a quantity that can no longer be held returns409 INVENTORY_EXHAUSTED. DELETEis idempotent: anitemIdthe cart does not hold returns the current cart.PATCHon one returns404withdetails.resourceTypeofcartItem.- Dropping the last extra leaves the customer’s tickets and hold expiry untouched.
Rules:
- Only extras can be added to, changed on, or removed from an existing cart. An
itemIdfor seats or GA returns400. To change seats, create a new cart withpreviousCartId(see Create Cart); to hold nothing at all,DELETE /api/v3/carts/{cartId}. occurrenceIdis the occurrence the item is for. Most extras run at the cart’s occurrence — send that. An extra with its own schedule (a guided tour, parking) carriesrequirements.occurrenceand runs at its own times — send the occurrence the customer picked for it, with thepriceOptionIdof the category they chose from that occurrence’sinventory-items. That occurrence must start at or afterrequirements.occurrence.earliestStartand beforelatestStart; one outside those bounds returns400 OCCURRENCE_NOT_COMPATIBLE, and one no longer on sale returns409 OCCURRENCE_NOT_AVAILABLE. Any other occurrence returns400 MIXED_OCCURRENCES_NOT_SUPPORTED.- Quantity is capped by the lowest of
quantityAvailable, the item’smaxQuantityPerOrder, and the price option’smaxQuantityPerOrder. Asking for more returns400. When several items in one request name the same extra, the cap applies to their summed quantity. - An extra with
minQuantityPerOrderneeds at least that many units, counted across its price options, by the time the order is placed. The cart accepts fewer while the customer edits it, so lines can be added, changed and removed in any order; placing an order that still holds fewer returns400 MINIMUM_QUANTITY_NOT_MET. - If the extras can no longer be held by the time the request reaches the venue’s system, the request returns
409 INVENTORY_EXHAUSTED. If the cart has expired, it returns410 CART_EXPIRED— create a new cart.
6. Checkout
Send an Idempotency-Key header: a UUID v4 you make for each attempt to buy. It is optional and strongly recommended, so a retry after a timeout never buys twice (see Idempotency Keys). contact.phone is optional, in E.164 format, unless your retailer needs one on every order. To choose whether TTG emails the customer a confirmation, send sendConfirmationEmail: false to stop it, true to send it. Leave it out to use your retailer’s default.
expectedTotal is the cart’s total, echoed back exactly — same amount, same currency. GET /api/v3/carts/{cartId} returns:
Send that object straight through as expectedTotal (as above) — don’t recompute it. A value that doesn’t match returns 400 with code EXPECTED_TOTAL_MISMATCH.
On success, the response is the order. Its tickets and barcodes are covered in After Checkout.
Paying
Pick payment.method from the cart, not from your own code. GET /api/v3/carts/{cartId} lists the methods that cart can be paid with in options.paymentMethods. The one exception is a cart with nothing to pay: send NONE, which the cart never lists.
The fields each method’s sub-object takes are on the Place an order endpoint in the API Reference. KLARNA and AFTERPAY are listed only on a cart read with country, and take two calls: see Klarna and Afterpay.
INVOICE is listed only when your retailer is set up to pay on account and the caller may pay on account: your secret key on its own, or your secret key together with an on-account access token. A publishable key is never offered INVOICE, and neither is a request that carries a customer’s token, because the permissions then come from that customer. An order that sends INVOICE anyway is refused with 400 PAYMENT_METHOD_NOT_AVAILABLE and nothing is charged (on a cart with nothing to pay, the refusal is 403 INSUFFICIENT_PERMISSIONS). Send only the method. An INVOICE payment takes no other fields:
NONE is for a cart that costs nothing, for example when a voucher covers the whole order. Check total.amount and send NONE only when it is 0. A voucher doesn’t always bring the total to zero — on a cart priced in another currency it can still leave a balance — so don’t guess from the voucher. NONE on a cart that still costs something is refused: with a non-zero expectedTotal, as 400 INVALID_FIELD_VALUE on payment.method; with 0, as 400 EXPECTED_TOTAL_MISMATCH.
An order that owes money and names a method the cart doesn’t list is refused with 400 PAYMENT_METHOD_NOT_AVAILABLE. The methods the cart takes are in details.availablePaymentMethods, and nothing is charged.
If the customer is checking out on a device you can see — a browser or a mobile app — describe it with the optional X-TT-Device-Data header, as JSON:
Sending it scores card payments for fraud on more signal, so fewer genuine customers are declined. A server-to-server integration with no customer device in the request path leaves it off; the order places normally without it.
Klarna and Afterpay
KLARNA and AFTERPAY are listed only when you read the cart with the country query parameter. Whether either can be offered depends on the shopper’s country and the amount to pay. Once you know where the shopper is, read the cart again with country (an ISO 3166-1 alpha-2 code, e.g. country=GB). The parameter doesn’t change the cart.
KLARNA takes two calls, because the shopper approves the purchase in Klarna’s own widget:
- Place the order with
method: KLARNA,gateway: ADYEN,klarna: { "country": "GB" }— the same country you sent on the cart request — andpayment.options.threeDSecure.returnUrl. Klarna is paid in that country, whatever the contact phone number is. It answers402with codePAYMENT_AUTHENTICATION_REQUIREDanddetails.action— a JSON string. Parse it and hand it to the Klarna SDK to show the widget. - When the shopper approves, place the order again, under a new
Idempotency-Key, with the same fields plus the two values the SDK returns, inpayment.options.threeDSecure.details:authorization_tokenandpaymentData. Senddetailsonly on this second call — omit it entirely on the first.
To send the shopper to Klarna’s own page instead of showing the widget — for example inside a mobile app, where Klarna does not allow its purchase flow in an embedded web view — add "flow": "REDIRECT" to klarna on both calls. The first call then answers with a details.action whose type is redirect, and the two calls follow the AFTERPAY steps below: send the shopper to the action’s url, and on the second call send the redirectResult they come back with in payment.options.threeDSecure.details. Leaving flow out, or sending WIDGET, keeps the widget.
If the second call never arrives, the authorization Klarna is holding is released, so an abandoned checkout doesn’t leave the shopper on the hook.
AFTERPAY also takes two calls, because the shopper approves the purchase on Afterpay’s own page — Clearpay in the UK, Cash App Afterpay in the US:
- Place the order with
method: AFTERPAY,gateway: ADYEN,afterpay: { "country": "GB" }— the same country you sent on the cart request — andpayment.options.threeDSecure.returnUrl. It answers402with codePAYMENT_AUTHENTICATION_REQUIREDanddetails.action— a JSON string whosetypeisredirect. Parse it and send the shopper to itsurl. - Afterpay sends the shopper back to your
returnUrlwith aredirectResultquery parameter. Place the order again, under a newIdempotency-Key, with the same fields plus that value inpayment.options.threeDSecure.details:{ "redirectResult": "…" }. Senddetailsonly on this second call — omit it entirely on the first.
The shopper pays the first installment when they approve, so the second call matters: if it is refused for good, or never arrives, the authorization Afterpay is holding is released and the installment is returned. A refusal you can fix leaves it open (see Checkout errors).
7. After Checkout
Keep the cart ID
The cartId is the credential for the order it becomes: any integration with a TTG API key that holds it can read that order, including the customer’s name, email address and tickets. Store it on your server with your own record of the order, and treat it like a password — don’t put it in links or logs.
Your API key alone can’t read an order: an integration reads one with the cart ID it was placed with. That includes orders a customer placed through TTG’s own checkout, which you can read through the API only if you have their cart ID.
Find out whether a checkout went through
After a timeout, a crash, or a 409 CART_ALREADY_ORDERED, look the order up by its cart:
data holds the order the cart produced, or is empty if the cart never became an order. The order’s status says what to do next:
Fetch the full order
This returns an order once it is placed, canceled or refunded. While it is still settling, or after its payment failed, it answers 404 — use the lookup above instead.
Cancel or refund an order
The API has no endpoint to cancel or refund an order. To cancel or refund one, contact TTG support. Fetch the order again afterward to see its new status.
status describes the whole order. When a cancellation gave the customer a credit voucher, the order’s cancellationVoucher holds it.
Tickets and barcodes
Each item’s tickets are in items[].tickets[]. A ticket’s barcode (value, format, optional imageUrl) is null until the order is paid and the venue releases its tickets, which some venues do only closer to the show. Fetch the order again later to pick barcodes up, and handle a null barcode in your UI.
Each item’s delivery says how the customer gets their tickets: delivery.method.name names the delivery method, and delivery.instructions (when present) tells the customer what to do. Show both to the customer.
Receipt Rendering
Both cart and order responses include receiptLines[]. Render them in order — do not compute totals client-side.
Each line has sign: positive = charge, negative = credit/discount. Each total.amount is a whole number, 0 or more.
Common Errors
For the full error format and retry strategy, see Error Handling.