Skip to content

Holds and confirmation

A reservation moves through one state machine, and only through it — nothing changes a reservation’s state any other way:

HELD → CONFIRMED → CHECKED_IN
HELD → CANCELLED
HELD → EXPIRED (hold timed out — 15 minutes)
CONFIRMED → CANCELLED
CONFIRMED → NO_SHOW

POST /v1/holds takes a signedQuoteToken from Quotes and turns it into a 15-minute hold. It requires an Idempotency-Key header: retry the same key and get the same hold back, never a second one. A pk_ key can create a hold (it’s the widget’s job); it can never read what’s inside one — reading a hold’s guest data needs GET /v1/bookings/{id} with an sk_ key.

POST /v1/holds/{id}/payment-intent creates a Stripe PaymentIntent for a held reservation. It needs an sk_ key and an Idempotency-Key header — retry the same key safely, get the same intent back. It returns a clientSecret, which the integrator hands to Stripe.js or Stripe Elements in the guest’s browser; the card is charged there, never on our servers. If the tenant hasn’t connected a Stripe account, this returns 409 stripe_not_configured.

When the card payment succeeds, Stripe sends payment_intent.succeeded to our webhook (POST /v1/webhooks/stripe), and the reservation moves HELD → CONFIRMED automatically. No separate confirm call is needed on this path.

POST /v1/holds/{id}/confirm exchanges a guest-verification token — see Verify a guest and confirm a hold — for a CONFIRMED reservation. Confirming is idempotent: calling it again on an already-confirmed hold returns the same result, not an error.

Confirming also requires a succeeded payment. confirm checks the guest-verification token and that a PaymentIntent for this hold has succeeded. Call it before the guest has paid, and it returns 409 payment_required instead of confirming. In practice, most bookings never need an explicit confirm call: the payment webhook confirms the hold as soon as the card charge succeeds. confirm is there to finalize a hold once payment already exists.

GET/PATCH/DELETE /v1/bookings/{id} reads a reservation and drives further legal transitions (check the guest in, check them out, mark a no-show, cancel). These need an sk_ key — a booking carries guest data, which a pk_ key can never read. PATCH only accepts checked_in, checked_out, no_show, or cancelled as a target state — confirmed is reachable only through the guest-token-gated confirm route above, never a plain PATCH.

Anyone can call POST /v1/holds with a publishable key found in a page’s source — that key is public by design. The only reason that isn’t a problem: a hold is worthless inventory-squatting until a real guest verifies their email and pays. Both of those steps are gated on our server, never the browser: verification through the OTP token, payment through the Stripe webhook — a pk_ key can trigger neither on its own.