Skip to main content
When a buyer picks tickets, reserve them first, then show the form. A hold takes the tickets out of everyone else’s available for the event’s hold window (default 10 minutes, event.hold_minutes). Checkout then takes the hold over, so the tickets the buyer was promised are the tickets they get. Two buyers can never both hold the last ticket. Holds are optional for the API but mandatory for a good storefront: without one, a buyer can spend five minutes filling in their details and get INSUFFICIENT_TICKETS at the end.
available on every ticket row already excludes other buyers’ holds. A ticket type can read “Agotado” with no ticket sold yet, and come back on sale within a minute when those holds expire. Don’t “correct” it client-side.

The flow

Session key

Every hold belongs to a session key: an opaque string of 8 to 128 characters that identifies the buyer’s browser, never the person. Generate it once (a UUID in an httpOnly cookie is the right place) and send the same value on every hold call. One session has one active hold per event: a new POST replaces the previous one. A hold is read or changed only with its own key.
Keep the session key server-side (a cookie your backend reads) when you can. A key that lives in localStorage lets one tab release another visitor’s reservation if it leaks.

1. Reserve

Show a countdown from expires_at. When it reaches zero the tickets are back on sale, and the only way forward is a new selection (which makes a fresh hold). Items accept the same optional fields as checkout items: performance_id for multi-date events and seat_ids for numbered seating.

Errors

2. Checkout with the hold

The order takes the hold over (ACTIVE → CONVERTED) and the payment window starts. If the hold ran out before submit you get 409 with code: "HOLD_EXPIRED" (or HOLD_NOT_ACTIVE if it was already used or released): show “tu reserva venció” and send the buyer back to the picker. Without hold_id, checkout/start reserves inline at that moment, exactly as it always did. That path still works for every storefront; it just loses the guarantee between “pick” and “pay”.

Lifecycle

GET /api/checkout/holds/{id} returns the current state (and expires a hold whose window is over). You rarely need it: the countdown and the checkout response tell you everything.