Bookings API
NewStays, parking and rentals booked on your own website, and paid on your own Stripe account.
/v1/bookingsNot counted in your quota5 endpointsFrom your server onlyThe Bookings module of your dashboard, on your own website. What you rent, seasons, prices, extras, closed dates and the questions you ask all come from your Bookings settings. Every booking lands in your Bookings calendar, and the guest in your Clients.
Every call names the project that has Bookings, with ?project=.
How a booking works
- Show the calendar with Setup: your rules, prices and what is already taken. Prices follow the same rules as the dashboard, so the page can show them as the guest picks dates, or ask Price a stay.
- Take the booking with Create a booking. We check the dates again, price the stay again from your settings (never from what the page showed), record the guest and hold the dates for your hold time.
- Send the guest to pay at
pay_url: Stripe Checkout on your own Stripe account, for what is due now (your reservation fee, plus extras charged at booking). When nothing is due now, the booking is confirmed at once and there is nopay_url. - Show the result. Stripe sends the guest back to your
return_urlwith?booking={uid}. Ask Read a booking: we check the payment with Stripe and answerconfirmedonce it went through. A guest who leaves Checkout lands on yourcancel_url: give the dates back with Release dates. - We confirm it even if the guest never comes back, through Stripe's notice to our webhook (see Stripe setup). The guest then gets your confirmation email and you get a New booking email, both from the address set in your Bookings settings.
payment_unavailable, so no guest ever holds dates they cannot pay for.Test mode
Add mode=test to Setup, Price a stay and Create a booking. Checkout then uses your Stripe test keys (saved in the dashboard next to the live ones), so you can pay with Stripe's test cards. A test booking is a real booking in your calendar, marked Test booking in its notes and "test": true here. Cancel it in the dashboard when you are done.
Stripe setup
Payments go to your own Stripe account, with the keys saved in your dashboard. In Stripe (Developers, Webhooks), add one endpoint, and a second one in test mode if you test:
- Address:
https://api.calenzy.app/webhooks/bookings/ - Events:
checkout.session.completedandcheckout.session.async_payment_succeeded, for payment methods that settle later, like PromptPay - Save its signing secret in the dashboard with your Stripe key. Without it, a booking is still confirmed when the guest comes back to your return page, but not when they close the tab first.
Setup
Everything the booking page shows, in one answer.
| projectRequiredinteger | The project that has Bookings |
| modestring | test for test mode. It decides online_payment. |
| openboolean | true when a booking can be taken. Show the page anyway, but take no booking while it is false |
| reasonstring | ok, or why it is closed: draft (Bookings not published in the dashboard), no_units, no_rates |
| booking_typestring | accommodation, parking or rental: tells you the words to use (night or day, guest or client) |
| currency, timezonestring | From your Bookings settings. Dates are counted in this timezone |
| min_nightsinteger | The shortest stay you take |
| reservation_feenumber | What is paid at booking |
| fee_modestring | deduct: the fee is part of the total and the rest is paid on arrival. add: the fee comes on top |
| season_rulestring | How a stay that crosses seasons is priced |
| hold_minutesinteger | How long dates stay held while the guest pays. Your setting, and at least 31 with online payment, since Stripe's payment page stays open 30 minutes |
| online_paymentboolean | Whether a booking with something due now can be paid (plan and Stripe key) |
| windowobject | from today to 18 months ahead: the period taken covers |
| unit_types[]array | What can be booked: id, code, name, capacity (most people, or null), units_total |
| unit_types[].takenarray | Ranges already booked or closed: from, to (exclusive), units |
| unit_types[].full_datesarray | Nights with nothing left, ready to strike out in a date picker |
| seasons[]array | id, code, name, the yearly range (from_month, from_day, to_month, to_day, which may wrap the year end) and is_default. The default season covers every date no other season does, so its range is null |
| rates[]array | Prices per unit_type, season and length of stay: nights is a bracket, price its price |
| addons[]array | Extras: id, code, name, price, price_unit (stay, night, week), charge_when (now or arrival) |
| questions[]array | What you ask the guest: key, label (per language), input_type, options, required |
curl "https://api.calenzy.app/v1/bookings/setup?project=7" \ -H "Authorization: Bearer YOUR_API_KEY"
{ "status": "ok", "open": true, "reason": "ok", "booking_type": "accommodation", "currency": "THB", "timezone": "Asia/Bangkok", "min_nights": 5, "reservation_fee": 3000.0, "fee_mode": "deduct", "season_rule": "lowest", "hold_minutes": 31, "online_payment": true, "window": { "from": "2026-10-08", "to": "2028-04-08" }, "unit_types": [ { "id": 1, "code": "bungalow", "name": "Bungalow", "capacity": 2, "units_total": 4, "taken": [ { "from": "2026-12-01", "to": "2027-03-01", "units": 1 } ], "full_dates": [] } ], "seasons": [ { "id": 1, "code": "high", "name": "High season", "from_month": 10, "from_day": 23, "to_month": 4, "to_day": 18, "is_default": false }, { "id": 2, "code": "green", "name": "Green season", "from_month": null, "from_day": null, "to_month": null, "to_day": null, "is_default": true } ], "rates": [ { "unit_type": 1, "season": 1, "nights": 7, "price": 21000.0 } ], "addons": [ { "id": 1, "code": "comfort", "name": "Comfort Add-On", "price": 3500.0, "price_unit": "week", "charge_when": "arrival" } ], "questions": [ { "key": "why_kohmak", "label": { "en": "Why Koh Mak?" }, "input_type": "textarea", "options": null, "required": false } ] }
Price a stay
The price of a stay, and whether the dates are free.
| projectRequiredinteger | The project that has Bookings |
| modestring | test for test mode |
| checkin, checkoutRequiredstring | YYYY-MM-DD. Check-out is the day the guest leaves |
| unit_typeinteger or string | An id or code from Setup. The first one when absent |
| addonsarray or string | Extras by id or code: a list, a comma separated string, or "all" |
| availableboolean | Whether the dates are free |
| quote.nightsinteger | Length of the stay |
| quote.seasonstring | The season code the stay is priced in |
| quote.mixed_seasonboolean | true when the stay crosses seasons |
| quote.accommodationnumber | The stay alone |
| quote.addons[]array | Each extra: id, code, name, quantity, unit_price, amount |
| quote.addons_totalnumber | All extras |
| quote.totalnumber | Stay and extras |
| quote.due_now, quote.due_arrivalnumber | What is paid at booking, and what is left for arrival |
{ "checkin": "2026-11-17", "checkout": "2026-12-17", "addons": ["comfort"] }
curl -X POST "https://api.calenzy.app/v1/bookings/quote?project=7" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"checkin": "2026-11-17",
"checkout": "2026-12-17",
"addons": ["comfort"]
}'{ "status": "ok", "available": true, "quote": { "checkin": "2026-11-17", "checkout": "2026-12-17", "nights": 30, "unit_type": 1, "currency": "THB", "season": "high", "mixed_season": false, "accommodation": 72000.0, "addons_total": 17500.0, "addons": [ { "id": 1, "code": "comfort", "name": "Comfort Add-On", "quantity": 5, "unit_price": 3500.0, "amount": 17500.0 } ], "total": 89500.0, "due_now": 3000.0, "due_arrival": 86500.0 } }
Create a booking
Hold the dates, record the guest, and get the payment page.
| projectRequiredinteger | The project that has Bookings |
| modestring | test for test mode |
| checkin, checkoutRequiredstring | As for Price a stay |
| unit_type, addons | As for Price a stay |
| first_name, last_name, emailRequiredstring | The guest |
| phonestring | As the guest typed it |
| party_sizeinteger | How many people, 1 when absent. Refused above the unit type's capacity |
| languagestring | The language the guest booked in (en, fr, de, zh-tw): their confirmation email and Checkout use it |
| arrival_time, arrival_travelstring | HH:MM, and how they travel. Shown in your New booking email |
| answersobject | Answers to your questions, {"question_key": "answer"}. Required when a question is |
| return_url, cancel_urlstring | https pages on your website. We add booking={uid} to both. Required when something is due now |
| guest_ipstring | The guest's IP address, kept with the booking |
| bookingobject | The booking as Read a booking shows it. status is hold until paid, or confirmed when nothing was due now |
| pay_urlstring or null | Stripe Checkout, to send the guest to now. null when nothing is due |
| expires_ininteger or null | Seconds the dates stay held for payment |
| quoteobject | The price charged, as for Price a stay |
{ "checkin": "2026-11-17", "checkout": "2026-11-24", "party_size": 2, "first_name": "Anna", "last_name": "Schmidt", "email": "anna@example.com", "phone": "+49 151 2345678", "language": "de", "arrival_time": "14:30", "answers": { "why_kohmak": "Quiet, the sea, and time to work." }, "return_url": "https://www.example.com/book/done", "cancel_url": "https://www.example.com/book/cancelled" }
curl -X POST "https://api.calenzy.app/v1/bookings/create?project=7" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"checkin": "2026-11-17",
"checkout": "2026-11-24",
"party_size": 2,
"first_name": "Anna",
"last_name": "Schmidt",
"email": "anna@example.com",
"phone": "+49 151 2345678",
"language": "de",
"arrival_time": "14:30",
"answers": { "why_kohmak": "Quiet, the sea, and time to work." },
"return_url": "https://www.example.com/book/done",
"cancel_url": "https://www.example.com/book/cancelled"
}'{ "status": "ok", "booking": { "ref": "CP-7KQ2MX", "uid": "3f9c1e7a5b2d48c6a0e4f81b29d7c635", "status": "hold", "test": false, "checkin": "2026-11-17", "checkout": "2026-11-24", "nights": 7, "party_size": 2, "currency": "THB", "total": 21000.0, "due_now": 3000.0, "due_arrival": 18000.0, "paid": 0.0 }, "pay_url": "https://checkout.stripe.com/c/pay/cs_live_...", "expires_in": 1860, "quote": { "nights": 7, "total": 21000.0, "due_now": 3000.0, "due_arrival": 18000.0 } }
Read a booking
One booking, with its payment checked.
Use the uid that Stripe adds to your return address, never the reference: the reference is short so a guest can read it out, the uid cannot be guessed. A booking still held with a Checkout is checked with Stripe first, and confirmed when the payment went through.
| uidRequiredstring | The booking identifier |
| projectRequiredinteger | The project that has Bookings |
| booking.ref, booking.uidstring | The reference the guest quotes, and the identifier for the API |
| booking.statusstring | hold (waiting for payment), expired (a hold whose time ran out), pending, confirmed, arrived, cancelled, no_show |
| booking.testboolean | Taken in test mode |
| booking.checkin, checkout, nights, party_size | The stay |
| booking.unit_typeobject | id and name |
| booking.currency, total, due_now, due_arrival, paid | The money |
| booking.addons[]array | name, quantity, amount |
| booking.guestobject | first_name, last_name, email, phone |
| booking.language, arrival_time, created_atstring | As recorded. created_at is in the project's timezone |
curl "https://api.calenzy.app/v1/bookings/3f9c1e7a5b2d48c6a0e4f81b29d7c635?project=7" \ -H "Authorization: Bearer YOUR_API_KEY"
{ "status": "ok", "booking": { "ref": "CP-7KQ2MX", "uid": "3f9c1e7a5b2d48c6a0e4f81b29d7c635", "status": "confirmed", "test": false, "checkin": "2026-11-17", "checkout": "2026-11-24", "nights": 7, "party_size": 2, "unit_type": { "id": 1, "name": "Bungalow" }, "language": "de", "currency": "THB", "total": 21000.0, "due_now": 3000.0, "due_arrival": 18000.0, "paid": 3000.0, "addons": [], "guest": { "first_name": "Anna", "last_name": "Schmidt", "email": "anna@example.com", "phone": "+49 151 2345678" }, "arrival_time": "14:30", "created_at": "2026-10-09 15:42:10" } }
Release dates
Give back the dates of a guest who left Checkout.
Call it from your cancel_url page. We close the guest's Stripe page first, so nobody can pay for dates that went back. Only an unpaid hold is released, and it becomes cancelled. A booking paid in another tab is confirmed instead.
| uidRequiredstring | The booking identifier |
| projectRequiredinteger | The project that has Bookings |
| releasedboolean | true when the dates went back |
| bookingobject | The booking, as Read a booking shows it |
curl -X POST "https://api.calenzy.app/v1/bookings/3f9c1e7a5b2d48c6a0e4f81b29d7c635/release?project=7" \ -H "Authorization: Bearer YOUR_API_KEY"
{ "status": "ok", "released": true, "booking": { "ref": "CP-7KQ2MX", "uid": "3f9c1e7a5b2d48c6a0e4f81b29d7c635", "status": "cancelled", "paid": 0.0 } }
Errors
A refused call takes no booking: every check runs before anything is written, and if Stripe fails once the dates are held, we cancel the hold at once. Besides these, any call can answer one of the common errors.
| Code | HTTP | When |
|---|---|---|
missing_project | 400 | No ?project= |
invalid_dates | 400 | Not YYYY-MM-DD, or check-out not after check-in |
past_dates | 400 | The check-in date has passed |
stay_too_short | 400 | Fewer nights than min_nights |
invalid_unit_type | 400 | A unit type that is not in this project |
invalid_addon | 400 | An extra that is not in this project |
invalid_guest | 400 | First name, last name or a valid email missing |
party_too_large | 400 | More people than the unit type's capacity |
missing_answer | 400 | A required question was not answered |
invalid_url | 400 | return_url or cancel_url missing, or not https |
bookings_not_configured | 404 | This project has no Bookings |
booking_not_found | 404 | No booking with this uid in this project |
bookings_closed | 409 | Bookings is not published, or has no units or prices (see reason in Setup) |
dates_unavailable | 409 | The dates were taken in the meantime |
unpriceable | 409 | No price is set for this stay |
save_failed | 500 | The booking could not be saved. Try again |
payment_unavailable | 503 | Something is due now but online payment is not available (plan, Stripe key, or Stripe did not answer). No booking was taken |
bookings_unavailable | 503 | A moment's problem on our side. Try again shortly |