Skip to content

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.

Network calls fail. Rules that keep retries safe:

  • POST /v1/holds always needs an Idempotency-Key header. 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 (200 instead of 201), never a duplicate.
  • POST /v1/holds/{id}/payment-intent also needs an Idempotency-Key header. Same rule as the hold itself: retry with the same key, get the same PaymentIntent back, never a second charge attempt.
  • POST /v1/bookings also needs an Idempotency-Key header. 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}/confirm is 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 returns 200 with 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 the guestVerificationToken from 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:

Terminal window
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_KEY

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.

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.

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.

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.

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.

  1. 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.
  2. Ask your operator contact for sk_live_/pk_live_ keys.
  3. Swap the key prefix only. Nothing else about the request shape changes.

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.

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.

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.