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

{
"data": [
{
"id": "v1_eyJzIjoiT1JDSC1BLTEwMSJ9",
"inventoryVariant": {
"_type": "AssignedSeat",
"id": "Orchestra-A-101",
"name": "Orchestra A 101",
"section": "Orchestra",
"row": "A",
"seat": "101"
},
"quantityAvailable": 1,
"priceOptions": [
{
"id": "eyJjIjoiRlVMTCJ9",
"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. 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": "123",
"inventoryItemId": "v1_eyJzIjoiT1JDSC1BLTEwMSJ9",
"priceOptionId": "eyJjIjoiRlVMTCJ9",
"quantity": 1
}
]
}

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

{
"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 }
],
"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:

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

Returns 201 with the updated cart. 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 that is not an extra’s, 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 naming a seat or GA item returns 400. To change seats, create a new 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 performance — send the cart’s occurrence. An extra with its own schedule (a guided tour, parking) runs at its own times — send the occurrence the customer picked for it. Any other occurrence returns 400.
  • 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.
  • 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
{
"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"
}
}

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

{ "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_EXPIREDCart timed out, was deleted, or no longer holds inventoryCreate a new cart
409CART_ALREADY_ORDEREDCart already became an order (possibly since cancelled)Fetch the order via GET /orders/{orderId} using details.orderId — do not buy again
409INVENTORY_EXHAUSTEDItems no longer availableBrowse inventory again

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