Hold a checkout reservation (and the timer that kills it)

Understand the hold: what reserves inventory, how long you get, and what happens when it lapses.

Owned by Member · 13 steps · about 15 minutes

Why this exists

This is the mechanism the whole ticketing side is built on, and it is worth ten minutes of anyone's time because almost every confusing checkout error traces back to it.

A cart reserves nothing. Adding an item writes a cart line with the price snapshotted at that moment, and takes zero inventory. Someone else can buy the last ticket while your cart sits open, and they should be able to — inventory belongs to whoever is actually trying to complete a purchase, not to whoever browsed first.

Checkout creates the hold. The reservation is where the tier's reserved count goes up and its available count goes down, all inside one write transaction with a database CHECK that makes overselling impossible even under contention: the check and the decrement cannot be separated, and whoever wins the write lock wins the ticket. The hold carries a TTL from the event (between 5 and 10 minutes; 10 by default) and the order shows it as a countdown.

An expired hold returns the inventory, and the return is committed before you are told. That ordering is deliberate: by the moment you see "hold expired", the seats are genuinely back on sale for everyone else rather than sitting in limbo while your browser catches up. You were not charged, because payment is the last step and it re-checks the hold before touching a card.

Finally, holds are rationed. Each user may keep only a small number of active holds per event (two by default), so a script cannot quietly sit on the room.

Before you start

  • A member session.
  • An event that is on sale with availability in at least one tier.
  • An admin session for the last three steps only.

Practise with

PersonaEmailPasswordNote
membermember@club.test member123the buyer
adminadmin@club.test admin123can see live holds and force the sweeper from the debug console

Steps 1–10 — Member

their manual →
  1. 1
    Open the event's Get tickets page and pick a tier and quantity.
    /events/{event_id}/checkout payments
    Expected result Live availability per tier, your cart for this event, and the refund policy that will apply to what you buy.
    Watch out for The quantity ceiling is the smaller of the tier's own per-order maximum and the platform-wide setting. The dropdown will not offer you more than is left.
  2. 2
    Add the tickets. Then wait a moment and reload the page.
    /api/cart/items POST payments
    Expected result A cart line at the current price. The tier's available count is unchanged.
    Watch out for Nothing is held. The price on the line is a snapshot, and it exists so that a price change between adding and checking out is caught and shown to you rather than silently charged.
  3. 3
    Open the cart page to see every event you have lines for, not just this one.
    /cart payments
    Expected result All open cart lines with their events.
  4. 4
    Check out. This is the moment inventory is actually taken.
    /api/checkout POST payments
    Expected result One reservation per event in the cart, an order awaiting payment, and a hold expiry stamped on it.
    Watch out for Three specific refusals live here and none of them charge anything: an empty cart; a price that changed since you added it (the error lists exactly which lines); and insufficient inventory (the error carries the real availability per tier). All three mean 'start again', not 'retry'.
  5. 5
    Learn the other route to the same place: the Reserve button on the public event page creates a hold directly and skips the cart entirely.
    /api/events/{event_id}/reservations POST events
    Expected result A reservation with the seconds remaining on it, then a redirect to the payment page carrying the reservation id.
    Watch out for A 429 here means you already hold the maximum number of active reservations for this event. Release one or let it lapse — a second browser tab does not get you a second allocation.
  6. 6
    Land on the payment page and watch the countdown.
    /checkout/{order_id} payments
    Expected result The order total with tax broken out, your available house credit, the payment methods, and the hold countdown.
    Watch out for This page accepts an order id OR a reservation id. Handed a reservation, it creates the order for you and redirects. Both URLs are legitimate — you are not lost.
  7. 7
    Read the hold itself while it is alive.
    /api/reservations/{reservation_id} events
    Expected result Status active, the per-tier lines with their snapshotted prices, and the seconds remaining.
  8. 8
    Change your mind properly: release the hold instead of walking away.
    /api/reservations/{reservation_id} DELETE events
    Expected result The inventory goes back immediately and the tier can leave sold-out state.
    Watch out for Releasing an already-committed or already-expired hold is a 409. That is not an error you need to fix — it means somebody or something got there first.
  9. 9
    Cancel the unpaid order (send no ticket ids). This is the order-level equivalent of releasing the hold.
    /api/orders/{order_id}/cancel POST payments
    Expected result The order is cancelled and its reservations are released.
    Watch out for If a payment attempt is in flight the cancel is refused with a conflict rather than racing it. The platform will not let you cancel an order that is mid-charge, because the money would be stranded.
  10. 10
    Now let a hold lapse on purpose and try to pay it.
    /api/orders/{order_id}/pay POST payments
    Expected result A 410 hold expired. Nothing was charged and the inventory is already back on sale.
    Watch out for This is the single most common support question on the platform. The answer is always the same: start again, you were not charged, and the seats you wanted may now be someone else's.

Steps 11–13 — Admin

their manual →

Watches the same hold from the inventory side and can force it to expire on demand.

  1. 11
    As an admin, look at what the sweeper would expire right now.
    Expected result The holds that are past their TTL and the inventory that would return.
    Watch out for Debug routes are admin-only and vanish entirely when debug endpoints are switched off. Do not build any habit that depends on them.
  2. 12
    Force the expiry so you can demonstrate the timer without waiting ten minutes.
    Expected result A list of cancelled order ids, and the tiers back on sale.
    Watch out for The sweeper and a live payment can race. That race is settled by the write lock rather than by luck: one of them wins, and there is no state where a hold is both committed and expired.
  3. 13
    Close the loop from the inventory side: list the active holds on the event with the members who own them.
    /api/admin/events/{event_id}/reservations events
    Expected result Live holds with expiry times and buyer emails.
    Watch out for Reserved and sold are different counters. A tier can read sold out purely because of holds and un-sell itself minutes later — that is normal, and it is why 'sold out' on this platform is a state, not a fact.