What happens when a payment arrives after the seat hold expires
The awkward edges of workshop booking software, from UPI handoffs to payment webhooks and check-in at the door.
Lanes Group · 2026-09-24

The easy version of a booking system is a button that takes a payment and reduces the number of seats by one. We have yet to see real bookings behave that neatly. Someone opens a UPI app, gets distracted by a bank OTP, returns to a browser tab that has gone stale, and completes a payment just as the temporary seat hold runs out. The money has moved. The interface may still say “pending.”
This is the sort of edge that decides whether a booking system is trustworthy. It came up while building software for a studio that sells classes and workshops. The code has a short hold for ordinary checkout, a longer one for staff-created payment links, and a separate path for confirming a booking when the payment provider calls back. Those are three clocks, not one.
The browser cannot be the final witness
A successful redirect is useful feedback for a buyer, but it is a fragile source of truth. The tab can close. The network can drop. A payment provider can retry a notification. We made the signed payment webhook responsible for the final state change. It checks the signature against the raw request body, looks up the payment, and confirms the associated booking.
That introduces another question: what if the same event arrives twice? The confirmation path has to recognize an already confirmed booking. The email path needs its own once-only claim. Otherwise a retry can issue duplicate passes or send a string of confirmations for a single order. We also had to account for a cart where one payment covers several workshop bookings, and for payment links whose provider-generated order ID differs from the ID stored when the link was created.
Releasing a seat is a state change, not a subtraction
The failure path is just as consequential. If a payment fails, the booking is cancelled and its seats are returned together. Releasing inventory while leaving the booking “pending” lets a later expiry job release the same seats again. The count may look plausible for days until a full class somehow sells extra places.
The hold is short because an abandoned checkout should not block a scarce seat indefinitely. It is long enough to survive ordinary app switching and payment approval. But a timer alone cannot decide what to do when the provider reports that someone has already paid. The paid state must be reconciled with inventory, even when the hold has lapsed.
The last test happens at the door
After payment, a QR code is only one part of admission. Older bookings may have two paid seats and only one attendee name. A roster that displays one person because it has one name would turn away a valid guest. The check-in logic therefore builds a row for every paid seat, marks unnamed seats honestly, and maps older unassigned arrival records to available seats without changing the total.
The takeaway is less glamorous than a new checkout animation: design the state transitions first. Write down who owns the truth for paid, failed, held, released, and admitted. The user sees a simple booking; the system earns that simplicity by handling the messy minutes around it.