Make a booking from your server
This is the production checklist once Quickstart works
end to end. It assumes you’re calling the API from your own backend with an
sk_ key.
Retry safely
Section titled “Retry safely”Network calls fail. Rules that keep retries safe:
POST /v1/holdsalways needs anIdempotency-Keyheader. Generate one UUID per booking attempt, not per HTTP request — if a request times out, retry it with the same key. You get the same hold back (200instead of201), never a duplicate.POST /v1/holds/{id}/payment-intentalso needs anIdempotency-Keyheader. Same rule as the hold itself: retry with the same key, get the same PaymentIntent back, never a second charge attempt.POST /v1/holds/{id}/confirmis naturally idempotent — confirming an already-confirmed hold returns the same result, not an error.- OTP request and OTP verify are naturally safe to retry too. Re-requesting a code just issues a new one; re-verifying the same correct code again returns the same token until it’s consumed.
Generate the key once, up front, and reuse it across every retry of the same attempt:
IDEMPOTENCY_KEY=$(uuidgen)
curl -X POST https://api.<domain>/v1/holds \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -H "Content-Type: application/json" \ -d '{ "signedQuoteToken": "eyJhbGciOi..." }'# on a timeout or connection error, retry with the SAME $IDEMPOTENCY_KEYconst idempotencyKey = crypto.randomUUID();
async function createHold() { const { data: hold, error } = await client.POST("/v1/holds", { headers: { "Idempotency-Key": idempotencyKey }, body: { signedQuoteToken: quote.signedQuoteToken }, }); if (error) throw error; return hold;}// on a timeout or connection error, call createHold() again —// idempotencyKey is unchanged, so you get the same hold backHandle the quote’s 15-minute clock
Section titled “Handle the quote’s 15-minute clock”A signedQuoteToken expires in 15 minutes (expiresAt in the quote
response). If your checkout flow can take longer than that — a guest steps
away, fills a long form — re-quote right before creating the hold rather
than holding onto a stale token.
Handle 409 unavailable the same way twice
Section titled “Handle 409 unavailable the same way twice”The same reasons vocabulary appears on GET /v1/availability and on
POST /v1/quotes’s 409. Write one reason-to-message mapping and reuse it
in both places — see Availability for the
full list.
Respect rate limits
Section titled “Respect rate limits”Go live
Section titled “Go live”- Confirm every call above works against
sk_test_/pk_test_keys, including the failure paths (bad code, expired quote, unavailable dates) — not just the happy path. - Ask your operator contact for
sk_live_/pk_live_keys. - Swap the key prefix only. Nothing else about the request shape changes.
Take payment before you confirm
Section titled “Take payment before you confirm”Card payment is live: POST /v1/holds/{id}/payment-intent creates a Stripe
PaymentIntent for a held reservation, sk_-only, with its own
Idempotency-Key requirement — same retry rules as the hold itself. Hand
the returned clientSecret to Stripe.js in the guest’s browser and confirm
the card there. See
Holds and confirmation for exactly
what confirm checks once payment is part of the flow.
What’s still coming
Section titled “What’s still coming”Outbound webhooks for booking events aren’t live yet — poll
GET /v1/bookings/{id} for now, rather than building a webhook receiver
against a guess.
Next steps
Section titled “Next steps”- Refund a booking — send money back after a cancellation or a change.
- Errors — the full error shape and status codes.