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

# Inventory Types

Every inventory item contains an `inventoryVariant` with a `_type` field that determines what kind of item it represents. This guide explains the four types and how to handle them.

Seats and general admission come from the inventory list of an occurrence (one date and time of an event). Extras (merchandise, drinks, parking) are offered per cart — they appear under `options.extras` on the cart response, not in the occurrence inventory. Gift cards come from the inventory list of a gift-card event (`productType: GIFT_CARD`), one item per denomination.

## Inventory Item

Each inventory item carries these fields, whatever its type:

| Field | Description |
|---|---|
| `id` | Inventory item ID — send it as `inventoryItemId` when you add the item to a cart |
| `inventoryVariant` | What the item is; its `_type` is one of the four types below |
| `priceOptions` | The prices the item can be bought at (see [Price Options](#price-options)) |
| `quantityAvailable` | How many units are in stock. `0` marks a sold-out general admission area kept on display: its `priceOptions` is empty, so show it as unavailable and don't add it to a cart. |
| `maxQuantityPerOrder` | Most units of this item one order may hold. Omitted when there is no cap. |
| `minQuantityPerOrder` | Extras only: the fewest units one order may hold (see [Extra](#extra)). Omitted when there is no minimum above `1`. |
| `selectionGroup`, `selectionBlocked` | Assigned seats only: how to sell the seat to a party (see [Seats for a party](#seats-for-a-party)) |
| `requirements` | Extras with their own schedule only: `requirements.occurrence` gives the `earliestStart` and `latestStart` of the occurrence the customer must pick for it (see [Add Extras](/guides/purchase-flow#5-add-extras-optional)) |

## The Four Types

| `_type` | What It Represents | `quantity` Rule | Tickets on Order |
|---|---|---|---|
| `AssignedSeat` | A specific seat (section, row, seat number) | Always `1` | 1 ticket |
| `GeneralAdmission` | Entry to a GA area (e.g., Standing, Balcony) | `1` to `quantityAvailable`, within the event's `orderLimit` | 1 per person |
| `Extra` | An add-on such as merchandise, drinks, or parking | `1` up to `quantityAvailable` and `maxQuantityPerOrder` | 1 per unit |
| `GiftCard` | One gift-card denomination (e.g., a £50 card) | Always `1`, one gift card per cart | None — the order item carries `giftCards[]` |

## Discriminator Pattern

All types share a common base (`id`, `_type`, `name`, `attributes`) and the `_type` field tells you which one you're dealing with. Any type can also carry `description` (plain text) and `extendedDescription` (rich text that may hold HTML or Markdown — sanitize it before rendering). Each type adds its own fields:

| `_type` | Adds |
|---|---|
| `AssignedSeat` | `section`, `row`, `seat`; `images` when inventory is requested with `include=IMAGES`; `entrance` on orders |
| `GeneralAdmission` | `venueAreaIds` |
| `Extra` | `imageUrl`, `caption` |
| `GiftCard` | `value` |

`section`, `row`, `seat` and `value` are always sent. The optional fields (`images`, `entrance`, `venueAreaIds`, `imageUrl`, `caption`) are left out when they have no value.

```json
{
  "inventoryVariant": {
    "_type": "AssignedSeat",
    "id": "ORCHESTRA-A-101",
    "name": "Orchestra A 101",
    "section": "Orchestra",
    "row": "A",
    "seat": "101",
    "attributes": []
  }
}
```

```json
{
  "inventoryVariant": {
    "_type": "GeneralAdmission",
    "id": "DANCE FLOOR",
    "name": "Dance Floor",
    "attributes": []
  }
}
```

```json
{
  "inventoryVariant": {
    "_type": "Extra",
    "id": "ICE CREAM",
    "name": "Ice Cream",
    "attributes": []
  }
}
```

```json
{
  "inventoryVariant": {
    "_type": "GiftCard",
    "id": "TODAYTIX GIFT CARD-5000",
    "name": "TodayTix Gift Card",
    "value": { "amount": 5000, "currency": "GBP" },
    "attributes": []
  }
}
```

**Key rule:** switch on `_type` to determine your rendering and quantity logic. Treat unknown `_type` values gracefully — new types may be added in the future.

## AssignedSeat

Represents a specific seat in a venue. Adds these fields:

| Field | Description | Example |
|---|---|---|
| `section` | Seating section | `"Orchestra"` |
| `row` | Row identifier | `"A"` |
| `seat` | Seat number | `"101"` |
| `images` | Views of the stage from the seat, each with a `url`. Only on inventory requested with `include=IMAGES`; empty for a seat with none. | `[{ "url": "https://…" }]` |
| `entrance` | Where the ticket holder enters, such as a door or gate. Only on orders, and left out when the venue assigns none. | `"Door 4"` |

Quantity is always `1` — each seat is a separate inventory item. To buy several seats, add one cart item per seat:

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

> Copy each ID whole from the inventory response, and never build one by hand. See [IDs](/guides/api-conventions#ids).

### Seats for a party

List inventory with the party size as `?quantity=` (see [Party Size](/guides/api-conventions#party-size)). Each seat in the response then says how to sell it:

| The seat carries | What to do |
|---|---|
| `selectionGroup.inventoryItemIds` | When the customer picks this seat, add every id in the list to the cart, one item per seat. The list includes the seat itself and is not always `quantity` seats long. |
| `selectionBlocked.chainSize` | The seat is in stock but isn't sold to a party of this size. Show it as unavailable, keep its price on display, and don't add it to a cart. `chainSize` is the party size that would make it buyable. |
| Neither | The seat can't be picked at this party size, for example because taking it would leave a single seat stranded. Show it as unavailable. |

Other seats a party of this size can't buy are left out of the list.

Without `quantity`, `selectionBlocked` is never sent and most seats carry no `selectionGroup`. A seat sold only in a fixed group, such as a box of four, carries its group either way: add the whole group whenever one is sent.

### When a seat selection is refused

A selection that would leave a single empty seat on its own is refused with `409 ORPHANED_SEATS`. The seats are free, but the venue's ticketing system won't sell them that way, so ask the customer to pick different seats. The venue's system makes this check when the seats are held, so it can refuse a selection that followed `selectionGroup`.

## GeneralAdmission

Represents entry to a general admission area. Adds one field:

| Field | Description | Example |
|---|---|---|
| `venueAreaIds` | The seating-chart areas this item sells into, matching the area ids on the venue's seating chart so you can highlight them. Left out when the item isn't tied to chart areas. | `["1204", "1205"]` |

Quantity can be `1` to `quantityAvailable` — a single inventory item covers multiple people. To buy 3 GA tickets, set `quantity: 3` on one cart item.

```json
{
  "items": [
    {
      "occurrenceId": "456",
      "inventoryItemId": "v1_eyJwcm92aWRlcklkIjoxMDAwLCJzZWF0SWQiOiJEQU5DRSBGTE9PUiJ9",
      "priceOptionId": "eyJpc0dBIjp0cnVlLCJwcm92aWRlclBhcmFtcyI6Int9IiwicHJpY2VUeXBlSWQiOiJBRFVMVCJ9",
      "quantity": 3
    }
  ]
}
```

On the order, you'll receive one ticket per person (3 tickets in this example). Each ticket's `barcode` is `null` until the venue releases it — see [After Checkout](/guides/purchase-flow#7-after-checkout).

## Extra

Represents an add-on sold alongside tickets — merchandise, drinks, parking, and similar. Adds these fields, each left out when the seller hasn't set it:

| Field | Description | Example |
|---|---|---|
| `imageUrl` | A picture of the extra | `"https://images.example.com/wine.jpg"` |
| `caption` | A short label, such as a serving size | `"175ml"` |

Extras are discovered on the cart, not on the occurrence. After you create a cart, the cart response lists the extras you can add under `options.extras`, each in the same inventory-item shape as seats and GA. An extra with its own schedule (a guided tour, parking) is found there too; its categories and prices for the time the customer picks come from that occurrence's `inventory-items`. Add one or several with `POST /api/v3/carts/{cartId}/items`, change how many with `PATCH /api/v3/carts/{cartId}/items/{itemId}`, and drop one with `DELETE /api/v3/carts/{cartId}/items/{itemId}` — see [Purchase Flow](/guides/purchase-flow#5-add-extras-optional).

```json
{
  "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
}
```

Quantity can be more than `1`, like GA. The cart refuses a quantity over either of these:

- `quantityAvailable` — how many are in stock.
- `maxQuantityPerOrder` on a price option — how many units one order may contain at that price option. Omitted when that option has no cap of its own.

The item also carries `maxQuantityPerOrder` — how many of this extra one order should contain, across all its price options. Omitted when there is no item cap. The cart doesn't check this one yet, so cap your quantity picker at it yourself.

Some extras also set a floor. `minQuantityPerOrder` on the item is the fewest units one order may hold, counted across all its price options: with a minimum of `4`, 2 Adult and 2 Child is enough. Omitted when the extra has no minimum above `1`.

An extra always appears as an order item and on the receipt, with one ticket per unit in `tickets[]`.

## GiftCard

Represents one denomination of a gift card. A £25 card and a £50 card are two inventory items. Has one additional field:

| Field | Description | Example |
|---|---|---|
| `value` | The balance the card is issued with. Separate from `priceOptions[].sellPrice`, which is what the customer pays: a card sold at a discount is still worth its full value. Always in the currency the card can be spent in — the event's base currency, never the one requested with `currency`. | `{ "amount": 5000, "currency": "GBP" }` |

A gift card's `name` is the name of its gift-card section, so every denomination shares it — show `value` to tell them apart. Its `id` looks like `TODAYTIX GIFT CARD-5000`, and can carry more after the value when two cards share a denomination. Like every ID, copy it as returned; don't build or parse it. In carts and orders a gift card's `id` is `null`.

A gift-card event's occurrence and venue are bookkeeping placeholders. Don't render a date, a time, or a venue for a gift card.

A cart that breaks one of these rules is refused — see [Error Handling](/guides/error-handling):

- `quantity` is always `1` (`GIFT_CARD_QUANTITY_NOT_SUPPORTED`).
- A cart holds one gift card; place a separate order for each card (`MULTIPLE_GIFT_CARDS_NOT_SUPPORTED`).
- Gift cards and tickets check out in separate carts (`MIXED_PRODUCT_TYPES_NOT_SUPPORTED`).
- A voucher can't be applied to a gift-card cart (`VOUCHER_NOT_APPLICABLE_TO_GIFT_CARD`).

On the order, a gift-card item carries no `tickets[]`. It carries `giftCards[]` instead, one entry per card issued: the `code` the recipient redeems, its face `value`, the `balance` left to spend (left out when the card doesn't track one), and `expiresOn`, the last day it can be used (left out when the card doesn't expire). Exactly one of `tickets[]` and `giftCards[]` is present on an order item.

```json
{
  "giftCards": [
    {
      "code": "GC123456789",
      "value": { "amount": 5000, "currency": "GBP" },
      "balance": { "amount": 5000, "currency": "GBP" },
      "expiresOn": "2027-10-14"
    }
  ]
}
```

The `code` is money: anyone who has it can spend the card. Show it only to the buyer. Your `cartId` reads the order back, code included, without the buyer signing in, so keep it on your server and never put it in a page, a link or a log.

## Inventory Attributes

All types can carry `attributes[]` — tags that describe characteristics of the inventory. Use these for display purposes (badges, filters, icons).

Each attribute has:

| Field | Type | Description |
|---|---|---|
| `name` | string | Display label (e.g., `"Restricted View"`, `"Premium"`) |
| `description` | string or null | Optional detail (e.g., `"Pillar may partially obstruct the stage"`) |
| `category` | enum | Rendering hint — see below |

### Attribute Categories

| Category | Meaning | Suggested Rendering |
|---|---|---|
| `POSITIVE` | Desirable trait | Green badge or checkmark |
| `NEGATIVE` | Limitation or restriction | Warning icon or amber badge |
| `NEUTRAL` | Informational | Plain text or gray badge |
| `FEATURED` | Highlighted / promoted | Star or highlight treatment |
| `ACCESSIBILITY` | Accessibility feature | Accessibility icon |

Example with attributes:

```json
{
  "inventoryVariant": {
    "_type": "AssignedSeat",
    "id": "Balcony-B-15",
    "name": "Balcony B 15",
    "section": "Balcony",
    "row": "B",
    "seat": "15",
    "attributes": [
      { "name": "Restricted View", "description": "Pillar may partially obstruct the stage", "category": "NEGATIVE" },
      { "name": "Wheelchair Accessible", "description": null, "category": "ACCESSIBILITY" }
    ]
  }
}
```

## Price Options

Each inventory item has one or more `priceOptions[]` — the available price categories for that item (e.g., Adult, Child, Senior).

| Field | Description |
|---|---|
| `id` | Price option ID (use when creating cart items) |
| `category.id` | Category ID, matched by `constraints.requiresCategory`. Omitted when the item has a single price. |
| `category.name` | Display name (e.g., `"Adult"`, `"Child"`) |
| `sellPrice` | All-in price the customer pays (fees and tax included) |
| `listPrice` | Original price before discount — display as strikethrough. Omitted when there's no discount, the saving can't be shown, and on cart and order items. |
| `discountPercentage` | Whole-number percentage saved, for a "Save 30%" label. Use it rather than working one out from `listPrice`. Omitted when there's no saving to show. |
| `discountAmount` | Amount saved, for when the saving may be shown as an amount but not as a percentage. Never sent with `discountPercentage`. |
| `fees` | The fees inside `sellPrice`, one per fee type. Never add them to a price. Omitted when the market doesn't require fee disclosure. |
| `maxQuantityPerOrder` | Most units one order may contain at this price option. Omitted when the option has no cap. |
| `constraints` | Selection rules. `constraints.requiresCategory` lists `category.id`s, at least one of which must also be in the order (for example, a Child ticket bought with an Adult one). Omitted when the option can be bought on its own. Only some venues' systems refuse an order that breaks the rule, so apply it in your own UI. |
| `isDefault` | `true` for the option to pre-select. Omitted when false. |
| `promotion` | The promotion that applies to this option. Omitted when none does. |

When `listPrice` is present, the customer is getting a deal:

```json
{
  "priceOptions": [
    {
      "id": "eyJwcm92aWRlckNvZGUiOiJGVUxMUFJJQ0UiLCJob2xkQ29kZSI6IjE1NzM5In0",
      "category": { "name": "Adult" },
      "listPrice": { "amount": 15000, "currency": "USD" },
      "sellPrice": { "amount": 12500, "currency": "USD" }
    }
  ]
}
```

Display: ~~150.00 USD~~ → **125.00 USD**

When there's no discount, `listPrice` is absent — just show `sellPrice`.

## Stable Variant IDs

Inventory variant `id` values are stable across all occurrences of the same event. The same seat returns the same variant `id` regardless of which date the customer picks. This is important if you're building seating charts or caching seat metadata. This is `inventoryVariant.id`, not the inventory item's own `id`, which you re-fetch before each cart (see [IDs](/guides/api-conventions#ids)).

A seat or general admission area also has the same `inventoryVariant.id` in inventory items, carts and orders, so you can match a cart or order line to the item the customer picked.

## Rendering Summary

| Scenario | Display |
|---|---|
| `_type: AssignedSeat` | Show section, row, seat. Quantity locked to 1. Add the whole `selectionGroup` when one is sent. With `?quantity=`, show a seat without one as unavailable. |
| `_type: GeneralAdmission` | Show name. Let customer pick quantity up to `quantityAvailable` and the event's `orderLimit.max`. Show an item with `quantityAvailable: 0` as sold out. |
| `_type: Extra` | Show name. Cap the quantity picker at the lowest of `quantityAvailable`, the item's `maxQuantityPerOrder`, and the selected price option's `maxQuantityPerOrder`. When `minQuantityPerOrder` is present, show it and don't let the customer add fewer than that many. |
| `_type: GiftCard` | Show name and `value`. Quantity locked to 1. Don't show a date, time, or venue. |
| `attributes[]` present | Render badges/icons using `category` for styling. |
| `listPrice` present | Show strikethrough original price next to `sellPrice`. |