Booking Service API Changes - v1.73.0
Release: v1.73.0 Date: 2026-09-04
Summary
Backward Compatible: MOSTLY — the booking v2 API gains endpoints, and lock, sale and directsale gain one new 409; the editor API removes one endpoint and changes one success response
New Endpoints: 11 on booking-service (6 public session, 4 private session, 1 session configuration) and 1 on editor-service (section photo status)
Modified Endpoints: v2 lock, sale and directsale answer 409 for a seat a live session holds; POST /api/section-photo/ answers 202 when the conversion queue is on; GET /api/instance-info gains two fields
Removed or Deprecated: GET /api/export/{id}/pdf/{eventId}/ removed; the converter’s POST /convert_pdf route removed with it
Migration Required: NO for integrations that do not use PDF export or call the section photo endpoint directly.
Booking sessions (SEAT-1226)
A booking session is a server-held cart for one event. Seats and general-admission capacity held by a session are owned by it: no other session, and no lock or sale from the existing endpoints, can take them while the session is live.
Lifecycle
| State | Holds inventory | Reached by |
|---|---|---|
ACTIVE |
yes | create; every lock and unlock keeps it here |
PENDING_PAYMENT |
yes | checkout; the cart is frozen and the payment grace period starts |
CONFIRMED |
no, sold | private confirm; every held line becomes SOLD in one transaction |
CANCELLED |
no | public or private cancel |
EXPIRED |
no | the hold time (ACTIVE) or the grace period (PENDING_PAYMENT) ran out |
Defaults are a fifteen-minute hold and a ten-minute payment grace; both are operator settings (see the Deployment Guide). The public session payloads carry expiresAt, expiresInSeconds and serverTime, so a page can run its own countdown without trusting the browser clock; create answers expiresAt and serverTime. The private session payload carries expiresAt and expiresInSeconds, and the list summaries carry expiresInSeconds alone.
Enabling public sessions
Every public session call, get and cancel included, answers 403 with NOT_ENABLED until the organisation enables public sessions:
PUT /api/private/management/v2.0/session-config/
X-API-Key: {organizationToken}
Content-Type: application/json
{ "publicSessionsEnabled": true }
The private session endpoints work regardless of this setting.
Public endpoints
Base path /api/public/v2.0/session/. No API key: the session id returned by create is the credential for every later call, so treat it as a secret and store it where a page reload can find it again. Like the other public endpoints, these are not part of the published OpenAPI document; this page and the renderer’s BookingSessionClient are the reference.
| Method | Path | Purpose |
|---|---|---|
POST |
/ |
Open a session. Body { "eventId": "...", "publicKey": "..." }, header Idempotency-Key required. Repeating the same key returns the same session. |
GET |
/{id} |
State, cart with captured prices, total and the expiry countdown. |
POST |
/{id}/lock |
Hold seats and general-admission capacity. Seats are additive and tolerate ids the session already owns; a GA capacity is the desired total for that area, so repeats are safe. All-or-nothing. |
POST |
/{id}/unlock |
Release part of what the session holds. Owner-scoped and idempotent: lines the session does not hold are a no-op. |
POST |
/{id}/checkout |
Lock anything in the (optional) body that is not held yet, then freeze the cart and start the payment grace. A repeat on a frozen session returns the same payload. |
POST |
/{id}/cancel |
Release the session. From ACTIVE it releases everything; a session already frozen for payment requires the body { "startOver": true }. |
create answers:
{
"sessionId": "2c1f0e3a-...",
"expiresAt": "2026-09-11T10:15:00",
"serverTime": "2026-09-11T10:00:00",
"maxSeats": 10,
"ttlSeconds": 900
}
maxSeats caps what one session may hold, counted as seats plus general-admission capacity; ttlSeconds is the hold time the countdown restarts from.
lock, unlock and checkout take a selection:
{
"seats": [1001, 1002],
"groupOfSeats": [{ "id": 77, "capacity": 2 }]
}
Each list holds at most 1000 entries. Every other public call answers the session:
{
"sessionId": "2c1f0e3a-...",
"eventId": "8f4b...",
"state": "ACTIVE",
"cart": {
"seats": [{ "id": 1001, "priceId": 12, "priceName": "Stalls" }],
"groupOfSeats": [{ "id": 77, "capacity": 2, "priceId": 14, "priceName": "Standing" }]
},
"total": 145.0,
"expiresAt": "2026-09-11T10:15:00",
"expiresInSeconds": 842,
"serverTime": "2026-09-11T10:00:58"
}
Prices are captured when a line is locked and carried on the cart from then on.
Private endpoints
Base path /api/private/v2.0/session/, authenticated with X-API-Key: {organizationToken} (the Secret API key from the Editor login response, sent verbatim).
| Method | Path | Purpose |
|---|---|---|
GET |
/ |
List sessions for an event, newest first. Query eventId (required), state (optional filter), limit (default 50, capped at 200), offset (default 0). |
GET |
/{id} |
The full session: id, eventId, state, cart, reference, total, expiresAt, expiresInSeconds, createdAt. The private payloads name the session id and carry no serverTime. |
POST |
/{id}/confirm |
Convert every held line to SOLD atomically and store the order reference. Body { "reference": "..." } (optional, 255 characters). Valid from PENDING_PAYMENT; a repeat on a CONFIRMED session returns the stored result. |
POST |
/{id}/cancel |
Release everything the session holds and mark it CANCELLED. Repeats on a CANCELLED or EXPIRED session return the current state. |
The list answers summaries: id, state, cartSize (seats plus GA capacity), total, createdAt, expiresInSeconds.
Errors
Errors use the Problem Details body the v2 API already returns, with errorCode and, where noted, extra members.
| Status | errorCode |
When | Extra members |
|---|---|---|---|
403 |
NOT_ENABLED |
Any public call, get and cancel included, for an organisation that has not enabled public sessions |
|
404 |
ENTITY_NOT_FOUND |
Unknown session id, or an eventId and publicKey pair that does not resolve on create |
|
409 |
SEAT_CONFLICT |
A lock or checkout line could not be acquired, or a held line can no longer be sold on confirm; the whole call was rolled back |
conflicts: { seats: [...], groupOfSeats: [...] } |
409 |
SESSION_FROZEN |
lock or unlock on a session frozen for payment |
state |
409 |
SESSION_DEAD |
lock, unlock or checkout on a CONFIRMED, CANCELLED or EXPIRED session, or on one whose hold has run out; cancel on a CONFIRMED session; confirm on a CANCELLED or EXPIRED one |
state |
409 |
SESSION_NOT_CHECKED_OUT |
confirm on a session that has not been checked out |
state |
409 |
START_OVER_REQUIRED |
Public cancel on a frozen session without startOver: true |
state |
422 |
EMPTY_CART |
checkout on a session that holds nothing |
|
429 |
CAP_SEATS |
lock or checkout that would take the session past maxSeats |
maxSeats |
get never fails on a dead session: it answers 200 with the stored state and expiresInSeconds: 0, so a page that reads a session back after a reload must check state before reusing the id. A repeat confirm on a CONFIRMED session and a repeat cancel on a CANCELLED or EXPIRED session also answer 200 with the stored state.
Effect on the existing booking endpoints
The v2 lock, sale and directsale endpoints now refuse a seat that a live booking session holds:
{
"status": 409,
"errorCode": "SESSION_HELD",
"detail": "Seats are held by an active booking session",
"conflicts": { "seats": [1001] }
}
The seat is reported rather than skipped, so a direct sale cannot silently drop part of an order. An integration that does not use sessions is unaffected: seats are only ever held by a session when one is created.
Operator switch
Operators can close the session endpoints with the Helm value global.bookingSessions.enabled (default true). While closed, the session endpoints are unavailable and existing holds still expire normally.
Section photos and conversions (SEAT-1238)
These are editor-service endpoints. They are not part of the Booking API integration surface and need an Editor login; they are listed because their contract changed for anything that drives the editor API directly.
POST /api/section-photo/
When the conversion queue is enabled (the default), the upload is accepted immediately:
HTTP/1.1 202 Accepted
{ "status": "accepted", "requestId": "..." }
Poll for the result:
GET /api/section-photo/{requestId}/
The answer is 200 with one of three statuses:
{ "status": "pending", "requestId": "..." }
{ "status": "ready", "requestId": "...", "url": "https://...", "thumbUrl": "https://..." }
{ "status": "failed", "requestId": "..." }
An unknown request id answers 400; a request id issued to another organisation answers 403. With the queue disabled the upload still answers 200 with status: "ready" and both urls directly, as before.
Removed: GET /api/export/{id}/pdf/{eventId}/
PDF export no longer exists, in the editor and in the API. Use the SVG export, GET /api/export/{id}/, which is unchanged. The converter’s POST /convert_pdf route, which never produced a document, is removed with it.
Converter routes
The converter’s own routes (POST /, /background, /thumbnail, /photo, GET /jobs/{id}) require Authorization: Bearer <value> and answer 401 without it. Editor-service sends the value itself; only a caller that talks to the converter directly needs it. /health and /metrics carry no token. Configuration is in the Deployment Guide.
Instance information (SEAT-1205)
GET /api/instance-info on editor-service gains two fields:
{
"installationId": "...",
"environment": "production"
}
environment is the value declared with the Helm value instance.environment, or unspecified when the deployment has not declared one.
Credential naming (SEAT-1231)
The OpenAPI document now describes the value sent in X-API-Key as the Secret API key from the Editor login response, and the renderer key as the Public API key, matching the names shown in Organization settings. Field names, header names and values are unchanged: send the value from the login response verbatim, and treat it as an opaque string.
Authentication Changes
The X-API-Key and X-Organization-ID header contract is unchanged.
The public session endpoints carry no API key. Opening a session requires the renderer public key of the event’s organisation; every later call is authenticated by the session id alone. Confirming a session is a private endpoint and needs the organisation’s Secret API key, so the confirm belongs on the integrator’s server, after payment.
Removed and Deprecated Endpoints
| Endpoint | Status | Replacement |
|---|---|---|
GET /api/export/{id}/pdf/{eventId}/ |
Removed | GET /api/export/{id}/ (SVG) |
POST /convert_pdf (converter service) |
Removed | none; it answered 501 |