Skip to content

Verify a guest and confirm a hold

Guest identity verification and hold confirmation are both live today. This guide covers the full escalation: a hold you already created, through to a confirmed reservation.

A reservationId from a POST /v1/holds response — see Quickstart.

Terminal window
curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/otp \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "email": "guest@example.com" }'

Returns 202 { "accepted": true, "expiresAt": "..." }. The guest receives a 6-digit code by email.

Terminal window
curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/otp/verify \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "email": "guest@example.com", "code": "482913" }'

Returns 200 { "guestVerificationToken": "...", "expiresAt": "..." }. The token is valid for 15 minutes.

Terminal window
curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/confirm \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "guestVerificationToken": "eyJhbGciOi..." }'

Card payment gates confirmation. POST /v1/holds/{reservationId}/payment-intent (sk_-only, Idempotency-Key required) returns a clientSecret for Stripe.js. Once the guest pays, our Stripe webhook confirms the hold automatically — you don’t need to call confirm yourself on that path. Call confirm early, before the payment succeeds, and it returns 409 payment_required instead.

If the guest pays you in cash or by bank transfer instead, no card payment happens. Your server (an sk_ key with bookings:write) or a dashboard admin confirms the hold with POST /v1/bookings/{id}/confirm. It does not need a verification token. See Holds and confirmation.

When a booking becomes confirmed, the guest gets a “Booking confirmed” email.

You seeWhyWhat to do
404 hold_not_foundWrong reservationId, or it belongs to another tenant’s keyDouble-check the ID and which key you’re using
409 hold_not_heldYou asked for a code, but the hold isn’t active — already expired, confirmed, or cancelledThere’s nothing to verify against an expired hold; make a new hold
400 bad_codeWrong code, an expired code (10 minutes), a code locked after 5 wrong guesses, or no active code for this hold and email. All of these look the same on purpose.Ask the guest to re-check their email, or request a new code. A locked code stays locked, even with the right digits.
429 rate_limitedToo many issue calls (5 per 15 minutes) or verify calls (10 per 15 minutes) for this holdBack off using the Retry-After header
400 invalid_guest_token (confirm)The token is malformed, expired, or wasn’t issued by usRe-run the OTP verify step to get a fresh token
400 wrong_reservation (confirm)The token is valid but was issued for a different reservationUse the token from the same reservationId you’re confirming
409 not_confirmable (confirm)The hold is no longer in a confirmable state (e.g. already cancelled or expired)Start a new hold — there’s no way to revive this one
409 payment_required (confirm)No succeeded Stripe payment exists for this hold yetCreate a payment intent and let the guest pay — the webhook confirms automatically once it succeeds