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_INHELD → CANCELLEDHELD → EXPIRED (hold timed out — 15 minutes)CONFIRMED → CANCELLEDCONFIRMED → NO_SHOWCreating a hold
Section titled “Creating a hold”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.
Paying for a hold
Section titled “Paying for a hold”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.
Confirming a hold
Section titled “Confirming a hold”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.
Reading and managing a booking
Section titled “Reading and managing a booking”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.
Why a pk_ hold isn’t a booking
Section titled “Why a pk_ hold isn’t a booking”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.