Seat States & Hold Types

Seatmap.pro tracks every seat in every event through a two-layer state model. The first layer is a small closed set of lifecycle states that the booking engine uses for concurrency and availability. The second layer is an open, customer-defined holdType that carries the business context – why is this seat held, who can release it, how long does it last, and how should it look on the seatmap.

The two layers

Layer Column Values Controls
Lifecycle state closed enum concurrency, availability
Hold type holdType open string TTL, release policy, payment, display

The booking engine only ever asks state = 'ACTIVE' to determine whether a seat can be locked. The holdType is metadata that governs the business flow around that seat.

Lifecycle states

Four states, fixed by the platform.

State Meaning Bookable TTL applies
ACTIVE Available for selection Yes No
LOCKED Held, pending action No Per hold type
SOLD Committed and fulfilled No No
BLOCKED Operationally unavailable No No

A typical cart flow moves a seat ACTIVELOCKEDSOLD. An abandoned cart moves it back LOCKEDACTIVE. A broken seat sits in BLOCKED until operations clears it.

Hold types

The holdType is a string you attach to a seat when it transitions out of ACTIVE. It lives alongside the lifecycle state and describes the reason the seat is held.

Platform defaults

Two hold types are shipped out of the box:

Hold type Valid state TTL Released by Payment
CART LOCKED 15 minutes customer, admin, system required
RESERVED LOCKED none admin not required

CART is the default for the v2 booking /lock endpoint – omitting holdType in the request body resolves to CART.

Defining your own hold types

Every organization can declare additional hold types via the JSONB config column on organization (and optionally inherit from the parent tenant). Configuration is read through the standard org/tenant config API used for other features like webhooks.

Example payload sent to POST /api/organizations/config/:

{
  "holdTypes": {
    "PARTNER_HOLD": {
      "validStates": ["LOCKED"],
      "ttlSeconds": 3600,
      "releaseApi": "ADMIN,API",
      "paymentRequired": true,
      "displayName": "Partner Hold",
      "displayColor": "#8B008B"
    },
    "COMP": {
      "validStates": ["LOCKED", "SOLD"],
      "ttlSeconds": null,
      "releaseApi": "ADMIN",
      "paymentRequired": false,
      "displayName": "Complimentary",
      "displayColor": "#FFD700"
    }
  }
}

Field reference:

Field Type Description
validStates string array Which lifecycle states this hold type is valid for. The booking service rejects requests that combine an invalid pairing.
ttlSeconds integer or null Auto-release window. null means the hold persists until explicitly released.
releaseApi string Comma-separated list of actors allowed to release: CUSTOMER, ADMIN, SYSTEM, API.
paymentRequired boolean Whether this hold must be followed by payment to convert to SOLD.
skipLockedState boolean When true, transitions directly ACTIVESOLD (used for box office flows).
displayName string Human-readable label shown in admin UIs.
displayColor string Hex color for renderer styling.
displayIcon string Optional image or icon reference for the renderer.

Tenant-level inheritance

If your organization belongs to a tenant, you can define hold types at the tenant level once and they apply to every organization in the tenant. Per-organization overrides take precedence field-by-field, so an organization can, for example, shorten the CART TTL without restating the full definition.

Resolution order for any hold type lookup:

  1. Organization-level config.holdTypes[name]
  2. Tenant-level config.holdTypes[name]
  3. Platform defaults

The resolver merges these three layers. If you override only ttlSeconds at the organization level, the rest of the definition is inherited from the tenant or the platform default.

Using hold types in the booking API

The v2 booking StateSelection payload accepts an optional holdType field:

POST /api/private/v2.0/booking/lock?eventId=123e4567-e89b-12d3-a456-426614174000
Content-Type: application/json

{
  "sessionId": "abc123",
  "seats": [{ "id": 42 }],
  "holdType": "PARTNER_HOLD"
}

The booking service validates:

  • The hold type is defined (as a platform default, tenant-level entry, or organization-level entry) for the calling organization.
  • validStates includes the target lifecycle state.

Unknown or mismatched hold types return 400 Bad Request. The /unlock and /revertsale endpoints do not take a holdType – returning a seat to ACTIVE clears whatever hold was attached.

Concurrency and the conditional lock

Every state transition is a conditional SQL update that asserts the expected current state in its WHERE clause. Two concurrent requests locking the same seat cannot both succeed – the second request’s update affects zero rows and the API returns false. This makes double-booking impossible at the database layer, regardless of cart flow timing.

Audit trail

Every successful state transition appends an immutable row to seat_state_events, recording:

  • Old and new lifecycle state
  • Hold type at the moment of the transition
  • Session id (when available)
  • Seat or group-of-seats affected
  • Timestamp

This append-only log is the source of truth for analytics, replacing the best-effort telemetry that preceded it. Downstream reporting can answer questions like “what percentage of CART holds converted to SOLD in the last week” without touching the booking hot path.

Renderer integration

Today, the booking renderer styles seats based on their lifecycle state (ACTIVE, LOCKED, SOLD). Custom holdType values are returned by the API and included in seat payloads, but a dedicated renderer.setSeatsState(seats, holdType) helper for applying customer-defined visuals is on the roadmap and not yet shipped. Until then, customers who want to visualize custom hold types can use the existing onBeforeSeatDraw callback to read the hold type from the seat data and apply their own fill or overlay.

  • Section States & Styling – the equivalent concept applied to section outlines (highlighted, selected, unavailable, filtered).