Migration: Areas to Inventory Items

Guide for migrating from the legacy GET /api/v4/products/{productId}/areas and detailed availability endpoints to the new GET /api/v3/events/{eventId}/occurrences/{occurrenceId}/inventory-items endpoint.

For the full v4 inventory reference, see Product Areas on the legacy portal.

Endpoint Comparison

Legacy (v4)New (v3)
MethodGETGET
Path/api/v4/products/{productId}/areas (+ /detailed availability)/api/v3/events/{eventId}/occurrences/{occurrenceId}/inventory-items
AuthaffiliateId headerX-TT-API-Key header
Calls per performanceTwo — detailed availability, then areasOne
ShapeAreas → groupings → price bandsFlat list of items, each typed

What Changed

One call, not two

The legacy flow used /detailed availability to size up a performance, then /areas to pull seats and price bands. v3 replaces both with a single call per occurrence that returns every purchasable item — seats and general admission alike.

# Legacy — two calls
GET /api/v4/availability/products/123/quantity/2/from/2026-03-15/to/2026-03-15/detailed
GET /api/v4/products/123/areas?date=20260315&time=1930&quantity=2
# New — one call
GET /api/v3/events/123/occurrences/456/inventory-items

You need the occurrence id first — get it from GET /api/v3/events/{eventId}/occurrences (see Availability → Occurrences).

From areas and price bands to typed items

Legacy nested seats inside areas, and areas inside price bands, controlled by groupBy (lump, priceBand). v3 returns a flat list. Each item carries an inventoryVariant with a _type that tells you what it is:

_typeLegacy equivalentQuantity rule
AssignedSeatA seat inside an area (row, number)Always 1
GeneralAdmissionAn unreserved area / lump1 to quantityAvailable

Switch on _type for rendering and quantity logic. For the full type reference, see Inventory Types.

Pricing: all-in sellPrice, not face value plus fees

Legacy split price into faceValue, orderFee, and an includesBookingFee flag, with a percentageDiscount on the side. You had to add them up. v3 gives you sellPrice — the all-in price the customer pays, fees included. When there’s a discount, listPrice carries the original for a strikethrough.

Legacy (v4)New (v3)Notes
faceValue + orderFeesellPriceAll-in, fees included — nothing to add up
percentageDiscountlistPrice presentShow listPrice struck through next to sellPrice
salePrice { value, currency, decimalPlaces }sellPrice { amount, currency }Minor units; get the scale from the currency
One price per bandpriceOptions[] (Adult, Child, Senior…)Each item can carry several price options

Price tiers become price options

Legacy price bands were a property of the area. v3 attaches priceOptions[] to each item — one entry per category (Adult, Child, Senior). Each has its own id, which you pass when you add the item to a cart.

Money format

1// Legacy
2{ "salePrice": [{ "value": 2500, "currency": "GBP", "decimalPlaces": 2 }] }
3
4// New
5{ "sellPrice": { "amount": 2500, "currency": "GBP" } }

Both are minor units, but v3 drops decimalPlaces — derive the scale from the currency code with your i18n library. See Money Format.

Seat attributes are always on

Legacy hid seat attributes behind includeSeatsAttributes=true. v3 always returns attributes[], each tagged with a category (POSITIVE, NEGATIVE, ACCESSIBILITY, NEUTRAL, FEATURED) so you can badge them consistently. See Inventory Types.

Response Shape

New (v3)

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 "attributes": []
13 },
14 "quantityAvailable": 1,
15 "priceOptions": [
16 {
17 "id": "eyJjIjoiRlVMTCJ9",
18 "category": { "name": "Adult" },
19 "listPrice": { "amount": 15000, "currency": "USD" },
20 "sellPrice": { "amount": 12500, "currency": "USD" }
21 }
22 ]
23 }
24 ],
25 "pagination": { "limit": 50, "offset": 0, "total": 3 }
26}

Key differences:

  • The response is paginated — items are in data[], with a pagination block (limit default 50, max 1000). Page with limit / offset until offset >= pagination.total. See Pagination.
  • The item id is a versioned, opaque token (v1_…) — pass it whole, with a priceOptionId, to POST /api/v3/carts. priceOptionId has no prefix. Never build either by hand.
  • Inventory item and price option IDs are ephemeral — re-fetch inventory to get fresh ones (see IDs)
  • inventoryVariant.id is stable across occurrences — the same seat returns the same variant ID on every date, safe to cache for seating charts

Migration Checklist

  1. Replace the two-call area/detailed flow with one inventory-items call per occurrence
  2. Get the occurrence id first from GET /api/v3/events/{eventId}/occurrences
  3. Switch on inventoryVariant._type instead of walking areas and price bands
  4. Read sellPrice as the all-in price — stop summing faceValue and orderFee
  5. Show listPrice struck through when it’s present; there’s no percentageDiscount to apply
  6. Read each item’s priceOptions[] for tiers; keep the priceOptionId for the cart step
  7. Drop decimalPlaces — derive the scale from the currency code
  8. Store the item id (the whole v1_… token) and priceOptionId as opaque, ephemeral values; re-fetch to refresh them
  9. Page the response with limit / offset until you’ve read pagination.total items

Next Steps

With inventory in hand, continue the Purchase Flow:

  1. Create CartPOST /api/v3/carts (see Baskets → Carts)
  2. CheckoutPOST /api/v3/orders (see Checkout → Orders)