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?quantity=2

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

The legacy quantity carries over as the optional ?quantity= query parameter: the response then only includes inventory a party of that size can buy together. Omit it to see all inventory, and call again when the customer changes the ticket count.

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

// Legacy
{ "salePrice": [{ "value": 2500, "currency": "GBP", "decimalPlaces": 2 }] }
// New
{ "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)

{
"data": [
{
"id": "v1_eyJzIjoiT1JDSC1BLTEwMSJ9",
"inventoryVariant": {
"_type": "AssignedSeat",
"id": "Orchestra-A-101",
"name": "Orchestra A 101",
"section": "Orchestra",
"row": "A",
"seat": "101",
"attributes": []
},
"quantityAvailable": 1,
"priceOptions": [
{
"id": "eyJjIjoiRlVMTCJ9",
"category": { "name": "Adult" },
"listPrice": { "amount": 15000, "currency": "USD" },
"sellPrice": { "amount": 12500, "currency": "USD" }
}
]
}
],
"pagination": { "limit": 50, "offset": 0, "total": 3 }
}

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 Cart — POST /api/v3/carts (see Baskets → Carts)
  2. Checkout — POST /api/v3/orders (see Checkout → Orders)