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": "..." }.
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.
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 |
Next steps
Section titled “Next steps”- Holds and confirmation — the full state machine and what each route checks.
- Errors — the full error shape.