Skip to navigation

Purchase Flow

Six steps (one optional) to browse, reserve, and purchase tickets via the TTG API, then what to do after checkout.

List Events → Browse Occurrences → Browse Inventory → Create Cart → Add Extras (optional) → Checkout → 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

GET /api/v3/events?locationId={locationId}

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

GET /api/v3/events/{eventId}/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

GET /api/v3/events/{eventId}/occurrences/{occurrenceId}/inventory-items

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:

TypeDescriptionQuantity Rule
AssignedSeatSpecific seat (section, row, seat)Must be 1
GeneralAdmissionGA area1 to quantityAvailable

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):

{
"data": [
{
"id": "v1_eyJwcm92aWRlcklkIjoxMDAwLCJzZWF0SWQiOiJPUkNIRVNUUkEtQS0xMDEifQ",
"inventoryVariant": {
"_type": "AssignedSeat",
"id": "ORCHESTRA-A-101",
"name": "Orchestra A 101",
"section": "Orchestra",
"row": "A",
"seat": "101"
},
"quantityAvailable": 1,
"priceOptions": [
{
"id": "eyJwcm92aWRlckNvZGUiOiJGVUxMUFJJQ0UiLCJob2xkQ29kZSI6IjE1NzM5In0",
"category": { "name": "Adult" },
"listPrice": { "amount": 15000, "currency": "USD" },
"sellPrice": { "amount": 12500, "currency": "USD" }
}
]
}
]
}

4. Create Cart

POST /api/v3/carts

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).

{
"items": [
{
"occurrenceId": "456",
"inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxMDAwLCJzZWF0SWQiOiJPUkNIRVNUUkEtQS0xMDEifQ",
"priceOptionId": "eyJwcm92aWRlckNvZGUiOiJGVUxMUFJJQ0UiLCJob2xkQ29kZSI6IjE1NzM5In0",
"quantity": 1
}
]
}

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 voucherCode on POST /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 same voucherCode on the occurrences and inventory-items requests, so the customer sees the dates and seats it unlocks. A code that can’t be used is ignored on POST /carts, with no error: the cart is created without it.
  • On a cart that already exists, send it to POST /api/v3/carts/{cartId}/vouchers as { "code": "SUMMER20" }. It returns 201 with the cart repriced, or 400 INVALID_VOUCHER when the code can’t be used. Some codes are limited to one use per customer: for a guest, add the customer’s email so 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.

{
"data": {
"options": {
"extras": [
{
"id": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
"inventoryVariant": { "_type": "Extra", "id": "ICE CREAM", "name": "Ice Cream", "attributes": [] },
"priceOptions": [
{ "id": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0", "category": { "name": "Adult" }, "sellPrice": { "amount": 4500, "currency": "USD" }, "maxQuantityPerOrder": 4 },
{ "id": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjEyIn0", "category": { "name": "Child" }, "sellPrice": { "amount": 1500, "currency": "USD" }, "maxQuantityPerOrder": 2 }
],
"quantityAvailable": 20,
"maxQuantityPerOrder": 6
}
]
}
}
}

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:

POST /api/v3/carts/{cartId}/items
{
"items": [
{
"occurrenceId": "456",
"inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
"priceOptionId": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0",
"quantity": 1
},
{
"occurrenceId": "456",
"inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
"priceOptionId": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjEyIn0",
"quantity": 1
}
]
}

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:

PATCH /api/v3/carts/{cartId}/items/{itemId} { "quantity": 2 }
DELETE /api/v3/carts/{cartId}/items/{itemId}

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.

  • quantity is the number to end up holding, up or down from the number held now. It is at least 1: to drop the item, send DELETE rather than PATCH with 0.
  • Raising a quantity is subject to the same limits as adding: above maxQuantityPerOrder returns 400, and a quantity that can no longer be held returns 409 INVENTORY_EXHAUSTED.
  • DELETE is idempotent: an itemId the cart does not hold returns the current cart. PATCH on one returns 404 with details.resourceType of cartItem.
  • 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 itemId for seats or GA returns 400. To change seats, create a new cart with previousCartId (see Create Cart); to hold nothing at all, DELETE /api/v3/carts/{cartId}.
  • occurrenceId is 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) carries requirements.occurrence and runs at its own times — send the occurrence the customer picked for it, with the priceOptionId of the category they chose from that occurrence’s inventory-items. That occurrence must start at or after requirements.occurrence.earliestStart and before latestStart; one outside those bounds returns 400 OCCURRENCE_NOT_COMPATIBLE, and one no longer on sale returns 409 OCCURRENCE_NOT_AVAILABLE. Any other occurrence returns 400 MIXED_OCCURRENCES_NOT_SUPPORTED.
  • Quantity is capped by the lowest of quantityAvailable, the item’s maxQuantityPerOrder, and the price option’s maxQuantityPerOrder. Asking for more returns 400. When several items in one request name the same extra, the cap applies to their summed quantity.
  • An extra with minQuantityPerOrder needs 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 returns 400 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 returns 410 CART_EXPIRED — create a new cart.

6. Checkout

POST /api/v3/orders
Idempotency-Key: 3f8c1e2a-9d4b-4f6a-8b21-7c5e0a1d2e34
{
"cartId": "6aac14d3-5826-4da8-98b6-9e68c28629d6",
"expectedTotal": { "amount": 18500, "currency": "USD" },
"contact": {
"name": "John Smith",
"email": "john.smith@example.com",
"phone": "+12125550123"
},
"payment": {
"method": "INVOICE"
}
}

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:

{ "data": { "total": { "amount": 18500, "currency": "USD" } } }

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.

{ "data": { "options": { "paymentMethods": [ { "method": "INVOICE" } ] } } }
methodWhat it isWhat the order sends with it
INVOICEBilled to your account and settled laterNothing else
CARDA card, freshly entered or savedgateway and card
APPLE_PAYApple Pay wallet tokengateway and applePay
GOOGLE_PAYGoogle Pay wallet tokengateway and googlePay
KLARNAPay later; the shopper approves in Klarna’s widget or on Klarna’s own pagegateway, klarna and a return URL, over two calls
AFTERPAYPay later; the shopper approves on Afterpay’s pagegateway, afterpay and a return URL, over two calls
NONENothing to pay — the cart’s total.amount is 0Nothing else

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:

"payment": { "method": "INVOICE" }

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.

{
"cartId": "6aac14d3-5826-4da8-98b6-9e68c28629d6",
"expectedTotal": { "amount": 0, "currency": "USD" },
"contact": { "name": "John Smith", "email": "john.smith@example.com" },
"payment": { "method": "NONE" }
}

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:

X-TT-Device-Data: {"device_id":"<your id for the browser or device>","device_type":"web","device_os":"ios"}
FieldValue
device_typeweb or native
device_osoperating system name, lowercase
device_idyour own identifier for the browser or device

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:

  1. Place the order with method: KLARNA, gateway: ADYEN, klarna: { "country": "GB" } — the same country you sent on the cart request — and payment.options.threeDSecure.returnUrl. Klarna is paid in that country, whatever the contact phone number is. It answers 402 with code PAYMENT_AUTHENTICATION_REQUIRED and details.action — a JSON string. Parse it and hand it to the Klarna SDK to show the widget.
  2. When the shopper approves, place the order again, under a new Idempotency-Key, with the same fields plus the two values the SDK returns, in payment.options.threeDSecure.details: authorization_token and paymentData. Send details only 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:

  1. Place the order with method: AFTERPAY, gateway: ADYEN, afterpay: { "country": "GB" } — the same country you sent on the cart request — and payment.options.threeDSecure.returnUrl. It answers 402 with code PAYMENT_AUTHENTICATION_REQUIRED and details.action — a JSON string whose type is redirect. Parse it and send the shopper to its url.
  2. Afterpay sends the shopper back to your returnUrl with a redirectResult query parameter. Place the order again, under a new Idempotency-Key, with the same fields plus that value in payment.options.threeDSecure.details: { "redirectResult": "…" }. Send details only 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:

GET /api/v3/orders?cartId={cartId}

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:

statusMeaningWhat to do
PROCESSINGThe payment is still settlingLook it up again shortly
FAILEDThe payment didn’t complete. This is final.Let the customer pay again: place the order again with POST /api/v3/orders on the same cart, under a new Idempotency-Key (a key that failed answers 409)
COMPLETEDThe order is placedFetch the full order (below)
CANCELLEDThe order was placed, then canceled. No credit voucher was issuedFetch the full order to show the customer
VOUCHER_ISSUEDThe order was placed, then canceled, and the customer was given a credit voucherFetch the full order: cancellationVoucher holds the voucher
REFUNDEDThe order was placed, then refundedFetch the full order to show the customer
nullNo payment status recorded yetTreat it like PROCESSING

Fetch the full order

GET /api/v3/orders/{orderId}?cartId={cartId}

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

StepHTTPError CodeCauseRecovery
Browse Inventory, Create Cart, Add Extras409OCCURRENCE_NOT_AVAILABLESales have closed for the occurrence — on Add Extras, the one picked for an extra with its own schedulePick another occurrence
Create Cart, Add Extras400MIXED_OCCURRENCES_NOT_SUPPORTEDThe items are for more than one occurrenceUse one occurrence per cart
Create Cart400MIXED_ITEM_TYPES_NOT_SUPPORTEDThe items mix general admission and assigned seatsUse a separate cart for each
Create Cart400EVENT_NOT_SELLABLEThe event is closed, or is sold only as an extra with another eventPick another event
Create Cart409ORPHANED_SEATSHolding these seats would leave a single seat on its ownChoose different seats
Create Cart, Add Extras409INVENTORY_EXHAUSTEDItems no longer availableBrowse inventory again
Vouchers400INVALID_VOUCHERThe code is unknown, expired, or not valid for these itemsShow message. For a guest, send email with a code limited to one use per customer
Add Extras, Checkout410CART_EXPIREDCart timed out, was deleted, or no longer holds inventoryCreate a new cart
Checkout400EXPECTED_TOTAL_MISMATCHexpectedTotal doesn’t match the cart totalRead the cart again and send its total exactly as returned, under a new Idempotency-Key
Checkout400PAYMENT_METHOD_NOT_AVAILABLEpayment.method or payment.gateway isn’t one the cart can be paid withPay with a method from details.availablePaymentMethods, under a new Idempotency-Key
Checkout400MISSING_REQUIRED_FIELDThis retailer needs a phone number, and contact.phone wasn’t sentCollect it and place the order again under a new Idempotency-Key
Checkout400INVALID_FIELD_VALUEA field can’t be used; param names it. With details.reason ORDER_PHONE_NUMBER_INVALID, the retailer can’t use the phone number; with ORDER_EMAIL_INVALID, the venue won’t accept the email addressFix the field (ask the customer for another number or a corrected email) and place the order again under a new Idempotency-Key
Checkout400INVALID_VOUCHERThe voucher on the cart failed its last check, after the payment was authorizedRemove the voucher, read the cart again, and place the order under a new Idempotency-Key
Checkout400MINIMUM_QUANTITY_NOT_METThe cart holds fewer of an extra than its minQuantityPerOrderAdd more of the extra, or remove it, then place the order under a new Idempotency-Key
Checkout400EVENT_MINIMUM_NOT_METThe cart holds fewer tickets than the event’s orderLimit.minCreate a new cart with at least the minimum, then place the order under a new Idempotency-Key
Add Extras, Checkout409CART_ALREADY_ORDEREDCart already became an order (possibly since canceled)Look it up with GET /orders?cartId={cartId} — do not buy again. See After Checkout.
Checkout402PAYMENT_AUTHENTICATION_REQUIREDThe shopper has to pass a card check, or approve a KLARNA or AFTERPAY paymentRun details.action, then place the order again with the result under a new Idempotency-Key. See Klarna and Afterpay
Checkout402PAYMENT_DECLINEDThe issuer refused the paymentShow message and let the customer pay another way, under a new Idempotency-Key
Checkout403RISK_CHECK_FAILEDRisk checks declined the orderDon’t retry, with any payment method. Show message and send the customer to customer support
Checkout403INSUFFICIENT_PERMISSIONSThe customer isn’t allowed to buy, or the key can’t use this payment method (such as INVOICE without your secret key)Customer: show message, don’t retry. Key: use the right credential (Error Handling)
Checkout409TICKET_LIMIT_EXCEEDEDThe venue limits how many tickets one customer can buy, counting earlier ordersShow message, which is the venue’s own wording and says how many tickets the customer has left, if any. Retrying the same order cannot succeed; a smaller order might

For the full error format and retry strategy, see Error Handling.