Purchase Flow

Five-step flow to browse, reserve, and purchase tickets via the TTG API, plus an optional step to add extras to the cart.

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

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 dates with pricing summaries, sold-out dates included. 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). An occurrence with a null fromPrice is never buyable.

The discount fields (maxDiscountPercentage or maxDiscountAmount) represent the best available discount across all price points. They may come from a different price point than fromPrice.

3. Browse Inventory

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

Returns purchasable items. Use inventoryVariant._type to determine the type:

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

For full details on each type, attributes, and pricing, see Inventory Types.

Pricing: sellPrice is the all-in price customers pay (fees included). listPrice appears only when a discount exists — display as strikethrough.

Example response (trimmed):

1{
2 "data": [
3 {
4 "id": "v1_eyJzIjoiT1JDSC1BLTEwMSJ9",
5 "inventoryVariant": {
6 "_type": "AssignedSeat",
7 "id": "Orchestra-A-101",
8 "name": "Orchestra A 101",
9 "section": "Orchestra",
10 "row": "A",
11 "seat": "101"
12 },
13 "quantityAvailable": 1,
14 "priceOptions": [
15 {
16 "id": "eyJjIjoiRlVMTCJ9",
17 "category": { "name": "Adult" },
18 "listPrice": { "amount": 15000, "currency": "USD" },
19 "sellPrice": { "amount": 12500, "currency": "USD" }
20 }
21 ]
22 }
23 ]
24}

4. Create Cart

POST /api/v3/carts

Reserves inventory and starts a hold timer. All items must be from the same occurrence. 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).

1{
2 "items": [
3 {
4 "occurrenceId": "123",
5 "inventoryItemId": "v1_eyJzIjoiT1JDSC1BLTEwMSJ9",
6 "priceOptionId": "eyJjIjoiRlVMTCJ9",
7 "quantity": 1
8 }
9 ]
10}

The response includes expiresAt (UTC) — display a countdown. When the cart expires, inventory is released and GET /api/v3/carts/{cartId} returns 404. Create a new cart to continue.

To release held inventory early: DELETE /api/v3/carts/{cartId} (idempotent, returns 204).

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 key is absent when the event offers none.

1{
2 "options": {
3 "extras": [
4 {
5 "id": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
6 "inventoryVariant": { "_type": "Extra", "id": "ICE CREAM", "name": "Ice Cream", "attributes": [] },
7 "priceOptions": [
8 { "id": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0", "category": { "name": "Adult" }, "sellPrice": { "amount": 4500, "currency": "USD" }, "maxQuantityPerOrder": 4 }
9 ],
10 "quantityAvailable": 20,
11 "maxQuantityPerOrder": 6
12 }
13 ]
14 }
15}

Add one to the cart with its inventory item ID and one of its price option IDs:

POST /api/v3/carts/{cartId}/items
1{
2 "occurrenceId": "123",
3 "inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxLCJzZWF0SWQiOiIxOCJ9",
4 "priceOptionId": "eyJpc0FkZE9uIjp0cnVlLCJwcmljZVR5cGVJZCI6IjExIn0",
5 "quantity": 2
6}

Returns 201 with the updated cart. The extra appears in items[] with an Extra inventory variant, and in the cart total and receiptLines[]. Sending the same extra and price option again increases its quantity.

Rules:

  • Only extras can be added to an existing cart. To change seats, create a new cart.
  • occurrenceId must match the cart’s occurrence — a mismatch returns 400.
  • Quantity is capped by the lowest of quantityAvailable, the item’s maxQuantityPerOrder, and the price option’s maxQuantityPerOrder.
  • If the extra is no longer available, the request returns 409. If the cart has expired, it returns 404 — create a new cart.

6. Checkout

POST /api/v3/orders
1{
2 "cartId": "6aac14d3-5826-4da8-98b6-9e68c28629d6",
3 "expectedTotal": { "amount": 18500, "currency": "USD" },
4 "contact": {
5 "name": "John Smith",
6 "email": "john.smith@example.com",
7 "phone": "+12125550123"
8 },
9 "payment": {
10 "method": "INVOICE"
11 }
12}

expectedTotal is the cart’s total, echoed back exactly — same amount, same currency. GET /api/v3/carts/{cartId} returns:

1{ "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.

The order settles in the cart’s currency; the POST /orders call takes no currency input, so to pay in another currency PATCH the cart first (see Currency).

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.

Integrating server-to-server? If there’s no end-user device in the request path, leave the header off. It’s optional and the order places normally without it.

On success, tickets are in items[].tickets[] — each with a barcode (value, format, optional imageUrl).

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. All total.amount values are positive integers.

Common Errors

HTTPError CodeCauseRecovery
400EXPECTED_TOTAL_MISMATCHexpectedTotal doesn’t match the cart totalSend the cart total exactly as returned
410CART_EXPIREDHold timed outCreate a new cart
409INVENTORY_EXHAUSTEDItems no longer availableBrowse inventory again

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