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/bookingsalso needs anIdempotency-Keyheader. It is the call that creates a booking for a guest in one step. Same rule: one key per attempt, reused on every retry.POST /v1/holds/{id}/confirmis naturally idempotent — confirming an already-confirmed hold returns the same result, not an error.POST /v1/bookings/{id}/confirm(paid offline) is also safe to repeat. The second call returns200with the same booking, and no second webhook or email is sent.- Requesting an OTP code is safe to retry. It just issues a new code (5 per
15 minutes per hold). Verifying is not repeatable: a correct code works
once. Verify it again and you get
400 bad_code. Keep theguestVerificationTokenfrom the first call (it lasts 15 minutes), and if you lost it, request a new code.
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”Lock your key to your server
Section titled “Lock your key to your server”Ask your operator contact to add your server’s IP addresses to the key’s
allowlist. Then a copied key is useless from any other address. A request
from an address that is not listed gets 404 ip_not_allowed. See
Authentication.
Save the guest’s contact
Section titled “Save the guest’s contact”Send a guest object (name, email, optional phone) with
POST /v1/holds. We save it on the booking. You read it back from
GET /v1/bookings/{id} as guestEmail, guestName, and guestPhone. Every
/v1/bookings response has these three fields. They are null when nobody
gave them. See
Holds and confirmation.
When the guest pays offline
Section titled “When the guest pays offline”If the guest pays in cash or by bank transfer, confirm the hold yourself:
POST /v1/bookings/{id}/confirm with { "paidOffline": true }. It needs
bookings:write. Without it, the hold expires after 15 minutes. See
Holds and confirmation.
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.
Get told when a booking changes
Section titled “Get told when a booking changes”Register an endpoint with POST /v1/webhook-endpoints and we send you a
signed event when a booking is confirmed or cancelled, a payment succeeds, or
a refund is made. Retries do not run on a timer yet, so keep
GET /v1/bookings/{id} as your way to catch up. See
Webhooks.
Selling time by the hour
Section titled “Selling time by the hour”The same steps work for an hourly space. Use GET /v1/resources/{id}/slots
instead of the availability call, and send the slot’s exact times to
POST /v1/quotes. See Hourly spaces and services.
Next steps
Section titled “Next steps”- Webhooks — stop polling.
- Refund a booking — send money back after a cancellation or a change.
- Errors — the full error shape and status codes.