Skip to content

Quickstart: your first booking

This guide takes you from a fresh API key to a confirmed booking. Every call matches the live /v1 surface exactly — copy, paste, and swap in your own IDs. The full hold → verify → pay → confirm flow is live, card payment included.

Examples are shown in curl, Node, PHP, Python, and TypeScript (using @booking-engine/sdk, a typed client generated from this same API spec). Pick whichever tab matches how you’re integrating. Every one of these calls runs on your server with a secret (sk_) key — never put an sk_ key in a browser. For a browser, use the widget with a pk_ key.

You don’t sign up for API keys yourself. Your operator contact gives you two keys after onboarding:

  • sk_test_… — your server’s key. Keep it off the client.
  • pk_test_… — safe to put in a browser (see Authentication for why).

Use the _test_ pair while you build. Test-mode keys hit the same API, against the operator’s sandbox. The dashboard issues live keys only, so ask your operator contact to get test keys for you.

Terminal window
curl https://api.<domain>/v1/ping
{ "ok": true, "ts": "2026-07-16T03:00:00.000Z" }

No key needed for this one call.

Terminal window
curl "https://api.<domain>/v1/availability?resource_id=<RESOURCE_ID>&from=2026-08-01T00:00:00Z&to=2026-08-04T00:00:00Z&party_size=2" \
-H "Authorization: Bearer sk_test_..."
{
"resourceId": "<RESOURCE_ID>",
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-04T00:00:00Z",
"partySize": 2,
"available": true,
"reasons": []
}

available: false comes with a non-empty reasons array — min_stay, stop_sell, reserved, and so on. See Availability for the full list.

Terminal window
curl -X POST https://api.<domain>/v1/quotes \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"resourceId": "<RESOURCE_ID>",
"ratePlanId": "<RATE_PLAN_ID>",
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-04T00:00:00Z",
"partySize": 2
}'

Response (trimmed):

{
"quoteId": "...",
"displayTotal": { "currency": "IDR", "amountMinorUnits": "4500000" },
"signedQuoteToken": "eyJhbGciOi...",
"expiresAt": "2026-07-16T03:15:00.000Z"
}

ratePlanId is required if the resource has room types — most lodging does. See Resources, room types, rate plans for how to look one up. The quote is only valid for 15 minutes: hold the stay before expiresAt, or re-quote.

Terminal window
curl -X POST https://api.<domain>/v1/holds \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: <a-uuid-you-generate>" \
-H "Content-Type: application/json" \
-d '{ "signedQuoteToken": "eyJhbGciOi..." }'
{
"id": "<RESERVATION_ID>",
"resourceId": "<RESOURCE_ID>",
"ratePlanId": "<RATE_PLAN_ID>",
"state": "held",
"startsAt": "2026-08-01T00:00:00.000Z",
"endsAt": "2026-08-04T00:00:00.000Z",
"totalMinorUnits": "4500000",
"currency": "IDR",
"holdExpiresAt": "2026-07-16T03:15:00.000Z",
"createdAt": "2026-07-16T03:00:00.000Z"
}

You can also send the guest’s contact in a guest object ({ "name", "email", "phone" }, with phone optional). We save it on the booking. It is optional with an sk_ key, and required with a pk_ key for an hourly space. See Holds and confirmation.

A hold expires in 15 minutes (holdExpiresAt). signedQuoteToken is the same one from step 3 — an invalid or expired token is a 400 (invalid_quote_token / quote_expired); a resource that’s busy with another concurrent hold, or already booked, is a 409 (resource_locked / hold_conflict). resource_locked carries Retry-After: wait that many seconds and retry with the same key.

Given the reservationId from step 4, request a one-time code:

Terminal window
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" }'
{ "accepted": true, "expiresAt": "2026-07-16T03:10:00.000Z" }

The guest receives a 6-digit code by email. Verify it:

Terminal window
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" }'
{
"guestVerificationToken": "eyJhbGciOi...",
"expiresAt": "2026-07-16T03:20:00.000Z"
}

A code works for 10 minutes. Five wrong guesses lock it, so the guest has to request a new one. A wrong, expired, or locked code all return the same 400 bad_code. Both calls are rate-limited per hold; a 429 carries Retry-After. Full failure-mode table: Verify a guest and confirm a hold.

Card payment is a real Stripe charge, and confirming a hold requires it. Create a PaymentIntent for the held reservation:

Terminal window
curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/payment-intent \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: <a-unique-uuid>" \
-H "Content-Type: application/json" \
-d '{}'
{
"reservationId": "<RESERVATION_ID>",
"stripePaymentIntentId": "pi_...",
"clientSecret": "pi_..._secret_...",
"amountMinorUnits": "4500000",
"currency": "IDR",
"state": "requires_payment"
}

Hand clientSecret to Stripe.js or Stripe Elements running in the guest’s browser, and confirm the card there with the tenant’s Stripe publishable key — a Stripe-issued key, separate from our own pk_. Your server never sees the card number.

When the card payment succeeds, Stripe sends payment_intent.succeeded to our webhook, and the reservation moves from held to confirmed automatically — no extra API call needed on the payment path. If the operator hasn’t connected a Stripe account yet, this call returns 409 stripe_not_configured.

Terminal window
curl -X POST https://api.<domain>/v1/holds/<RESERVATION_ID>/confirm \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "guestVerificationToken": "eyJhbGciOi..." }'
{
"id": "<RESERVATION_ID>",
"resourceId": "<RESOURCE_ID>",
"ratePlanId": "<RATE_PLAN_ID>",
"state": "confirmed",
"startsAt": "2026-08-01T00:00:00.000Z",
"endsAt": "2026-08-04T00:00:00.000Z",
"totalMinorUnits": "4500000",
"currency": "IDR",
"holdExpiresAt": null,
"createdAt": "2026-07-16T03:00:00.000Z"
}

Re-confirming an already-confirmed hold is safe — same response, no error. A token minted for a different reservation is a 400 wrong_reservation; an invalid or expired one is 400 invalid_guest_token; a hold that’s no longer confirmable (already cancelled or expired) is 409 not_confirmable.

GET /v1/bookings/{id} needs an sk_ key — it returns guest data (guestEmail, guestName, guestPhone), which a pk_ key can never read.

Terminal window
curl https://api.<domain>/v1/bookings/<RESERVATION_ID> \
-H "Authorization: Bearer sk_test_..."

The response has the hold fields from step 4, plus the guest’s contact and a few more fields:

{
"id": "<RESERVATION_ID>",
"resourceId": "<RESOURCE_ID>",
"ratePlanId": "<RATE_PLAN_ID>",
"state": "confirmed",
"startsAt": "2026-08-01T00:00:00.000Z",
"endsAt": "2026-08-04T00:00:00.000Z",
"totalMinorUnits": "4500000",
"currency": "IDR",
"holdExpiresAt": null,
"createdAt": "2026-07-16T03:00:00.000Z",
"roomTypeId": "<ROOM_TYPE_ID>",
"occupancyAdults": null,
"occupancyChildren": 0,
"quantity": 1,
"source": "api",
"guestEmail": "guest@example.com",
"guestName": null,
"guestPhone": null,
"updatedAt": "2026-07-16T03:05:00.000Z"
}

guestEmail is the email the guest verified in step 5. guestName and guestPhone are null here, because step 4 sent no guest object. If you send guest with the hold, they hold the name and phone you sent. occupancyAdults is null because step 4 sent no partySize. PATCH drives further transitions (checked_in, checked_out, no_show, cancelled); DELETE cancels the booking. Both 404 for an unknown or cross-tenant id, and 409 for an illegal transition (e.g. cancelling an already-checked-out booking).

When the booking becomes confirmed, the guest gets a “Booking confirmed” email at the saved email address.

  • Building a browser checkout instead of a server flow? See Embed the widget — the same availability → quote → hold → pay flow, pre-built as a copy-paste embed, using a Stripe-hosted checkout page instead of Stripe Elements.
  • Just want an availability calendar on a page, no booking flow? See Embed the calendar.
  • Selling meeting rooms, desks, or services by the hour? See Hourly spaces and services.
  • Want your server told when a booking changes, with no polling? See Webhooks.
  • Need to send money back after a booking is cancelled or changed? See Refund a booking.
  • Want field-by-field detail? See API Reference.
  • Something 4xx’d? See Errors.