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.
The flow
Section titled “The flow”- Start with a
reservationIdfrom aPOST /v1/holdsresponse — see Quickstart. - Request a code:
POST /v1/holds/{reservationId}/otpwith{ "email": "<guest email>" }. Returns202 { "accepted": true, "expiresAt": ... }. The guest receives a 6-digit code by email. - Verify it:
POST /v1/holds/{reservationId}/otp/verifywith{ "email": "<same email>", "code": "<6 digits>" }. Returns200 { "guestVerificationToken": "...", "expiresAt": ... }. - Confirm the hold:
POST /v1/holds/{reservationId}/confirmwith{ "guestVerificationToken": "..." }. This token only authorizes confirming this one reservation — it won’t work for any other reservation or email. Returns200with the reservation, nowstate: "confirmed"— but only once a card payment has succeeded on this hold (see step 5). See Holds and confirmation for what confirm checks today. - Card payment gates confirmation.
POST /v1/holds/{reservationId}/payment-intent(sk_-only,Idempotency-Keyrequired) returns aclientSecretfor Stripe.js. Once the guest pays, our Stripe webhook confirms the hold automatically — you don’t need to callconfirmyourself on that path. Callconfirmearly, before the payment succeeds, and it returns409 payment_requiredinstead.
Troubleshooting
Section titled “Troubleshooting”| You see | Why | What to do |
|---|---|---|
404 hold_not_found |
Wrong reservationId, or it belongs to another tenant’s key |
Double-check the ID and which key you’re using |
409 hold_not_held |
The hold isn’t active — already expired or already confirmed | There’s nothing to verify against an expired hold; a new one needs a new OTP cycle |
400 bad_code |
Wrong code, or no active code for this hold+email | Ask the guest to re-check their email, or request a new code |
400 expired |
The code’s own TTL passed | Request a new code |
423 exhausted_attempts |
Six wrong guesses in a row locked this code | Request a new code — the old one won’t unlock, even with the right digits |
429 rate_limited |
Too many issue/verify calls for this hold | Back off using the Retry-After header |
400 invalid_guest_token (confirm) |
The token is malformed, expired, or wasn’t issued by us | Re-run the OTP verify step to get a fresh token |
400 wrong_reservation (confirm) |
The token is valid but was issued for a different reservation | Use 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 yet | Create a payment intent and let the guest pay — the webhook confirms automatically once it succeeds |
A note on email matching
Section titled “A note on email matching”Verify with the exact same email you requested with. Matching is
case-insensitive on our side, but a typo between the two calls always fails
as bad_code, even if the digits are right.