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.
Prerequisites
Section titled “Prerequisites”A reservationId from a POST /v1/holds response — see
Quickstart.
The flow
Section titled “The flow”1. Request a code
Section titled “1. Request a code”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" }'const { data: otp, error } = await client.POST("/v1/holds/{reservationId}/otp", { params: { path: { reservationId: hold.id } }, body: { email: "guest@example.com" },});if (error) throw error;Returns 202 { "accepted": true, "expiresAt": "..." }. The guest receives a
6-digit code by email.
2. Verify it
Section titled “2. Verify it”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" }'const { data: verified, error } = await client.POST("/v1/holds/{reservationId}/otp/verify", { params: { path: { reservationId: hold.id } }, body: { email: "guest@example.com", code: "482913" },});if (error) throw error;Returns 200 { "guestVerificationToken": "...", "expiresAt": "..." }. The
token is valid for 15 minutes.
3. Confirm the hold
Section titled “3. Confirm the hold”curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/confirm \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "guestVerificationToken": "eyJhbGciOi..." }'const { data: confirmed, error } = await client.POST("/v1/holds/{reservationId}/confirm", { params: { path: { reservationId: hold.id } }, body: { guestVerificationToken: verified.guestVerificationToken },});if (error) throw error;4. Pay
Section titled “4. Pay”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.
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 | You asked for a code, but the hold isn’t active — already expired, confirmed, or cancelled | There’s nothing to verify against an expired hold; make a new hold |
400 bad_code | Wrong 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_limited | Too many issue calls (5 per 15 minutes) or verify calls (10 per 15 minutes) 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 |
Next steps
Section titled “Next steps”- Holds and confirmation — the full state machine and what each route checks.
- Errors — the full error shape.