> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.todaytixgroup.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.todaytixgroup.com/_mcp/server.

# API Conventions

Patterns and standards that apply across all TTG API v3 endpoints. Read this first — individual endpoint docs assume familiarity with these conventions.

## Response Envelope

All responses use a consistent envelope:

**Success:**
```json
{
  "data": { ... }
}
```

**Success (list):**
```json
{
  "data": [ ... ],
  "pagination": { "limit": 50, "offset": 0, "total": 142 }
}
```

**Error:** an `error` object instead of `data`. Its fields are in [Error Handling](/guides/error-handling#error-response-structure).

A body holds `data` or `error`, never both. Lists add `pagination` beside `data`, and some add more, such as `filters` on occurrences. A `204` answer, such as from `DELETE /api/v3/carts/{cartId}`, has no body. Parse the HTTP status code first (2xx vs 4xx/5xx), then read the body.

## Money Format

All monetary values use **integer minor units** with an ISO 4217 currency code. No floating-point, no strings.

```json
{
  "amount": 18500,
  "currency": "USD"
}
```

The number of decimal places depends on the currency:

| Currency | Scale | `amount: 18500` means |
|----------|-------|-----------------------|
| USD, GBP, EUR | 2 | $185.00 |
| JPY, KRW | 0 | ¥18,500 |

Use your platform's i18n library to determine the scale from the currency code — don't hardcode `/ 100`.

**Client examples:**
- **JavaScript:** `new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount / 10 ** scale)`
- **iOS:** `NumberFormatter` with `currencyCode` set
- **Android:** `Currency.getInstance(currency).defaultFractionDigits` for scale

## Currency

Every price can be shown and charged in a local currency. The default is the event's **base currency**: the currency the event is priced in, which is its location's own currency.

The rule: **reads take a `currency` query parameter (display); writes take a `currency` body field (state)**.

**Browsing.** On occurrences (an event's dates and times) and inventory-items, send `?currency=` (ISO 4217) to see prices in that currency; omit it for the base currency. This affects display only. An event's `fromPrice` is always in its base currency, so `GET /api/v3/events` takes no currency.

**Which currencies can I ask for?** Each location on `GET /api/v3/locations` lists its `availableCurrencies` — the base currency first, then the alternatives available to you. Build currency pickers from this list rather than probing. The same list also comes back as `details.supportedCurrencies` when `400 CURRENCY_NOT_SUPPORTED` rejects a currency the retailer doesn't sell (an unrecognized code is rejected before retailer lookup and carries no details).

**A cart owns its currency.** A cart is created in a currency — base by default, or the body's `currency` when supported. The cart's current currency is `total.currency` on the response; `options.currencies` lists the currencies it can be priced in.

- **Quote:** `GET /api/v3/carts/{cartId}?currency=EUR` returns the cart priced in euros, without changing the cart. `total.currency` then says `EUR`, not the cart's own currency, so a quoted total won't pass as `expectedTotal` until you switch the cart. Read it again without the parameter and you get the cart's own currency back.
- **Switch:** `PATCH /api/v3/carts/{cartId}` with `{ "currency": "EUR" }` changes the currency the cart is priced and settled in. Later reads come back in euros with no parameter. Send the event's base currency to go back to base prices.

**Prices are always current.** Every amount is worked out from the exchange rate at the time you read the cart, so a total can move while the cart is open. Read the cart again just before checkout and send that amount as `expectedTotal`.

**`total` is the amount owed** — don't derive it from the items, the receipt lines, or the voucher.

**Orders confirm the cart.** `POST /api/v3/orders` settles in the cart's currency — the order call takes no currency input, so switch currency on the cart (`PATCH`) beforehand. `expectedTotal` must equal the cart `total` exactly, both `amount` and `currency`. A different amount returns `400 EXPECTED_TOTAL_MISMATCH`: read the cart again and place the order with the new total, under a new `Idempotency-Key` if you sent one. A different currency returns `400 INVALID_FIELD_VALUE` on `expectedTotal.currency`.

Asking for a currency the retailer doesn't sell the event in returns `400 CURRENCY_NOT_SUPPORTED`. So does a code the API doesn't know: it knows a fixed list of currencies, so even a real ISO 4217 code outside it (such as `INR`) gets this error. A value that isn't three capital letters (such as `gbp`) returns `400 INVALID_FIELD_VALUE`. A currency the retailer *does* sell but that can't be priced right now (no current exchange rate) returns `503 CURRENCY_TEMPORARILY_UNAVAILABLE` — retry shortly. This applies wherever a currency is accepted: browsing, cart create (`POST`), cart quote (`GET`), and cart switch (`PATCH`). Asking for nothing prices in the cart's own currency — the base currency unless the cart was created in or switched to another one. A requested currency is never silently swapped for base.

**Example — a London show (base `GBP`), customer paying in euros:** create the cart, `PATCH` it with `{ "currency": "EUR" }`, read the cart to get the current euro `total`, then place the order with `expectedTotal` set to it — e.g. `{ "amount": 4899, "currency": "EUR" }`.

## Party Size

Browse reads take a `?quantity=` query parameter (a positive integer) — the number of tickets the party needs. Omit it to see everything.

- **Inventory-items:** general admission areas without enough tickets drop out, and so do most seats the party can't buy. A seat that stays listed can still be unavailable at that size: [Seats for a party](/guides/inventory-types#seats-for-a-party) explains how to tell.
- **Occurrences:** pricing and `availabilityLevel` reflect only that inventory. An occurrence without enough seats together reads as `SOLD_OUT`, or is left out when the event or your retailer account is set to list only dates with something on sale.
- **Changing the count:** call again when the customer changes the ticket count. Pagination applies to the filtered list, so fetch every page with the same quantity.

## IDs

All IDs are **opaque strings**. Never parse, construct, or derive meaning from an ID — treat them as tokens you receive from one endpoint and pass to another. The format may change without notice.

**Stable IDs** — safe to store:
- Events, Occurrences, Carts, Orders, Tickets

A cart ID is also a credential: store it on your server only — it reads the order. See [Keep the cart ID](/guides/purchase-flow#keep-the-cart-id).

**Re-fetch, don't store** — these don't expire and aren't tied to a session, but they point at seats and prices that change. A price can change while its price option ID stays the same, so an unchanged ID doesn't mean an unchanged price. Re-fetch inventory just before adding to a cart:
- Inventory Items, Price Options

## Pagination

All v3 list endpoints — events, occurrences, inventory-items, locations, and orders by cart (`GET /api/v3/orders?cartId=`) — use `limit`/`offset` pagination and return a `pagination` block.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `limit` | integer | 50 | Max results per page (1–1000) |
| `offset` | integer | 0 | Number of results to skip |

The response includes a `pagination` object:

```json
{
  "pagination": {
    "limit": 50,
    "offset": 0,
    "total": 142
  }
}
```

To iterate all pages: increment `offset` by `limit` until `offset >= total`.

## Timezones

**Occurrence times** (`startsAt`, `endsAt`) use the venue's local timezone with UTC offset:
```
2026-03-15T19:30:00-04:00
```

Use the `venue.timezone` field (IANA identifier, e.g., `America/New_York`) if you need an IANA timezone for calendar integration (Apple Calendar, Google Calendar). It can be `null`; then fall back to the offset in `startsAt`. For display, the offset embedded in the timestamp is sufficient — no timezone manipulation required.

**System timestamps** (`createdAt`, `expiresAt`) use UTC:
```
2026-03-15T19:40:00Z
```

Whole days are plain `yyyy-MM-dd` dates: an event's `startDate` and `endDate`, and `expiresOn` (the last day it can be used) on a cancellation voucher (an order's `cancellationVoucher`) or a gift card, such as `2027-04-20`.

## Nullability

Fields are nullable only when absence carries meaning:

- `availabilityLevel: null` — no availability signal; display the occurrence normally
- `fromPrice: null` — nothing is on sale for this occurrence (sold out, off sale, or not yet on sale); it can't be bought
- `endsAt: null` — the occurrence has no fixed end time
- `startDate: null` — the event's run dates aren't announced yet
- `endDate: null` — the event's run is open-ended
- `barcode: null` — barcode not yet available (delayed delivery venues)

When a nullable field is `null`, handle it explicitly in your UI — don't display "null" or "$0.00".

Optional fields that carry no information are **omitted entirely** rather than sent as `null`. For example:

- `maxDiscountPercentage` — absent when no discount exists, or the event doesn't show savings as a percentage
- `listPrice` — absent when there's no discount to show (see [Price Options](/guides/inventory-types#price-options))
- `totalTaxes` — absent when the taxes on a cart or order add up to zero

## Forward Compatibility

The API may add new fields, enum values, or error codes at any time without a version bump. To avoid breaking:

- **Ignore unknown fields** — don't fail on unexpected keys in response objects
- **Handle unknown enum values** — use a default/fallback case for enums like `status`, `_type`, and error `code`
- **Don't switch on error messages** — use `type` and `code` for programmatic handling; `message` is for display only and may change