Holds and idempotency
A hold keeps a place while a guest decides or pays. Pass hold_minutes to bookings_create and the booking comes back held with an expires_at; bookings_confirm makes it confirmed and bookings_cancel frees it. Pass an idempotency_key, a UUID you generate, so a retry returns the same booking instead of booking twice.
States
heldThe place is kept until expires_at, and counts against capacity until then.confirmedBooked. Without hold_minutes, bookings_create confirms at once.cancelledThe place is free again. A payment taken online is refunded in full, unless bookings_cancel's refund says a part or none.A hold past expires_at stays held but no longer takes a place: there's no expired status. Each change writes booking.held, booking.confirmed, booking.moved or booking.cancelled; a hold keeps its expires_at when it moves.
Hold a place
Pass hold_minutes, 5 to 1440. The booking comes back held, with its expires_at.
curl -X POST https://daybooked.io/api/v1/bookings \
-H "Authorization: Bearer daybooked_live_…" \
-H "Content-Type: application/json" \
-d '{
"offering_id": "8f1c2e4a-0b7d-4c55-9a7e-3f2d1c0b9a88",
"starts_at": "2026-12-05T15:00:00Z",
"quantity": 2,
"customer": {
"email": "pat@example.com",
"name": "Pat"
},
"hold_minutes": 30,
"idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
}'{
"id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
"reference": "0GWYRZ2V",
"status": "held",
"expires_at": "2026-12-01T17:34:11+00:00",
"starts_at": "2026-12-05T15:00:00+00:00",
"quantity": 2,
"total_cents": 7000,
"idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
}The widget holds a place for 35 minutes while a guest pays on Stripe Checkout, and the payment confirms it. That guest has passed bot checks and is paying, so the hold counts against capacity only.
A guest who leaves Checkout without paying can hear from you once: from 10 minutes to a day after the hold lapses, when Stripe says the Checkout expired unpaid, bookings_nudge emails them a link to finish booking, while the time is still free. The Reminders automation sends it on its own when its Unpaid holds switch is on.
Confirm or cancel
Confirm before expires_at. Confirming twice returns the booking unchanged; past expires_at, a hold confirms only if its place is still free.
curl -X POST https://daybooked.io/api/v1/bookings/1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b/confirm \
-H "Authorization: Bearer daybooked_live_…"{
"id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
"reference": "0GWYRZ2V",
"status": "confirmed",
"expires_at": "2026-12-01T17:34:11+00:00",
"starts_at": "2026-12-05T15:00:00+00:00",
"quantity": 2,
"total_cents": 7000,
"idempotency_key": "5f0f0a52-3a2e-4f1c-9a53-0d8a1e9e7b11"
}bookings_cancel frees the place of a hold or a booking. With refund: "none" keeps a payment made online, { amount_cents } refunds that part (refund_cents keeps it). Cancelling twice changes nothing. To call off a whole time and let its guests pick another, sessions_cancel takes guests: "choose" (below).
Deposits
An offering can ask guests who pay online at booking for a deposit to hold their place: deposit_cents, an amount, or deposit_percent, 1 to 99 of the total, on offerings_create or offerings_update. One or the other: setting one clears the other, and both at once is 400 deposit_conflict. Without one, guests pay the whole total.
- Checkout takes the deposit of the total after any promo code: a percent rounds half up to a whole minor unit, an amount is at most the total. The booking keeps what was paid (
paid_cents) and what Checkout charged (charged_cents); the rest is due at the start, a rental's at pick-up, as the guest's page, the emails and your dashboard say. bookings_set_paidwithpaid: truerecords the rest paid by hand:paid_centsbecomes the total.paid: falsetakes back only that rest, never the deposit.- Cancelling refunds what was paid online, the deposit:
bookings_cancel'srefundtakes a part up tocharged_cents(more is 400refund_too_large), and a guest's own cancel or a time called off refunds all of it. - A deposit applies where guests pay online at booking (Stripe connected, on a paid plan): the booking page, the widget and an assistant's hold, through the same Checkout. A reseller's guests pay the reseller.
Gift cards
A business sells gift cards on its booking page (Settings → Gift cards, while it takes payments at booking) or issues them with gift_cards_issue, on any plan. A guest spends one by giving its code as gift_card_code to bookings_create, beside any promo_code.
- A card's code is 12 characters of Crockford base32, read in any case, spaced or not. What's on it is the sum of its entries, which
gift_cards_getlists (purchase or issue, redeem, refund, adjust):balanceis that sum,availablewhat it can spend now. Cards never expire. - A card pays what's due at booking after the promo code: the offering's deposit where it asks one, else the whole total, all it can spend at most. The booking keeps
gift_card_idandgift_cents, which count as paid. - The card is taken when the booking is confirmed: at once when nothing is left to pay, or once the rest is paid on Checkout and the hold confirmed. A hold keeps its part from other bookings while it lasts, and one that lapses never takes it. A resent
idempotency_keynever takes it twice. - Cancelling a booking a card paid for puts its part back on the card, whoever cancels and whatever
refundsays about a payment made online. gift_cards_adjustfixes a balance by a signed amount with a reason, never below zero. Nothing deletes a card. A reseller's bookings and an assistant's holds take no codes.
Calling off a time
sessions_cancel cancels a time and refunds everyone booked on it. Pass guests: "choose" to let each guest pick another time of the same offering instead, and note for a line in your words in their email.
- Holds and a reseller's bookings are cancelled as before. Each other booking stays
confirmed, keeps its reference and payment, and getschoose_by: the end of the 7th day after, in your time zone. - Each logs
booking.called_off, and its guest gets one email with yournote. On their booking page they pick another time with room for their party (booking.moved), or take the refund. - Whoever hasn't picked by
choose_byis cancelled (booking.cancelled): what they paid online is refunded in full; money you took by hand is yours to give back. You can move or cancel any of them yourself meanwhile. - To end the wait sooner,
sessions_cancelagain withguests: "refund"cancels whoever is still picking; once more changes nothing. - A booking waiting on a pick holds no place, and gets no reminders.
Idempotency keys
- Generate a UUID per booking and send it as
idempotency_key. - A retry with the same key and the same request returns the original booking, even if the slot has filled since, and writes no second event.
- The same key with a different request is refused with
idempotency_mismatch. - The request is the offering, session (a course's
series_id), start, quantity, periods, customer,hold_minutes,promo_codeandgift_card_code; names, phones, notes and answers don't count.
Hold limits
Open holds made over the API and MCP are capped, so no agent or script can sit on your inventory. Past a cap, a hold is refused with too_many_holds:
- 3 open holds per guest email
- 20 open holds per API key or OAuth client
- Half of any one slot's capacity, rounded up
Errors
Errors come back as JSON with the HTTP status; MCP tools return them as code: message.
too_many_holds429From bookings_create. The hold would pass a cap. Confirm or cancel one first, or book outright.idempotency_mismatch409From bookings_create. The key was already used for a different booking.sold_out409From bookings_create. Not enough left at that time.answers_required400From bookings_create. The offering asks a required question (offerings_get's questions) and answers has nothing for it; question_id names it.invalid_answer400From bookings_create. An answer doesn't fit its question: a choice that isn't one of its options, say; question_id names it.code_invalid400From bookings_create. The promo_code isn't one of yours, is switched off, hasn't started yet or is for another offering.code_expired400From bookings_create. The promo_code's end has passed.code_used_up400From bookings_create. The promo_code has been used as often as its limit allows.gift_card_invalid400From bookings_create. The gift_card_code isn't one of your cards, or was made in another currency.gift_card_empty400From bookings_create. The gift card has nothing left on it.gift_card_short409From bookings_create. The gift card's balance moved since it was checked: another booking spent it first. bookings_confirm says the same for a hold whose card was spent since.plan_limit402From bookings_create. The business has had its plan's bookings this month: new ones, holds included, pause until it changes plan (upgrade_url) or the month turns.hold_expired409From bookings_confirm. The hold ran past expires_at and its place was taken.booking_cancelled409From bookings_confirm. The booking was cancelled, so it can't be confirmed.{
"error": {
"code": "hold_expired",
"message": "This hold expired and its place was taken"
}
}Waitlist
A full time can keep a waitlist. Switch it on per offering with offerings_update and waitlist_enabled: times on a calendar only, a class's or an appointment's on its calendar.
- On the booking page and the widget, a time with no room for the guest's party stays visible and offers the waitlist: they give their name, email and party, and nothing is held or paid. They get one email saying so. Every waitlist email carries their link to leave while they're on it.
- When a place opens (a cancellation, a hold that lapsed, more capacity), daybooked's regular check emails everyone waiting whose party fits, all at once, and the first to book gets it: nothing is held for anyone. A guest hears at most once every 6 hours and 3 times in all, never inside the offering's notice or after the time starts.
- A confirmed booking for the time, or one moved into it, takes a guest off its waitlist; a hold doesn't. A cancelled time ends its waitlist and emails each guest, whether or not the switch is still on. A change to a calendar entry leaves a time someone waits for as it is, as it leaves a booked one, and nobody is emailed. Switching the waitlist off with
offerings_updatetakes everyone waiting off it and emails each of them a link to book another time. - When Fill the day opens the next day's time for a full one, or one with people waiting, everyone waiting whose party fits is emailed the new time with a link to book it, and stays on the waitlist they joined.
waitlist_offerdoes the same for any other time of the offering, once per guest and time. - One inbox gets at most 10 waitlist emails a day from every business together, however its address is spelled. Joining from the booking page allows 10 joins a day per inbox and 40 per IP address, with each business.
waitlist_listshows who is waiting, for asession_idor anoffering_id's times to come.waitlist_addputs a guest on by hand and emails them (409has_roomwhen their party fits now: book it instead; 400too_many_placesfor a party the time could never take), and says whether the email went;waitlist_removetakes one off. Joining again from the booking page changes nothing;waitlist_addchanges a guest's party.waitlist.joined,waitlist.leftandwaitlist.openedcarry ids and counts, never names.
Courses
A class can sell as a course: one booking takes a seat in every time of one calendar entry, from its first to come, at the class's price per seat for the whole course. Switch it on with offerings_update and course: true (a class only: 409 not_a_class), or Sell as a course in the offering's rules.
- A course sells each calendar entry that ends: one that doesn't repeat, or a repeat with an end date (
ends_ononseries_createandseries_update, which any repeating entry may now have). A course's repeating entry without one is refused with 409invalid_entry. availability_getgives one slot per entry with times to come, whatever dates are asked:series_id,starts_at(its first time to come),ends_at(its last time's end),sessions(how many),repeat, andremaining, the seats left in its fullest time.bookings_createtakes the slot'sseries_id(400series_requiredwithout it). The booking holds a seat in every scheduled time of the entry from its first to come, all checked at once: one seat short in any of them refuses the whole booking withsold_out. It comes back withseries_id, andsession_idis its first time. Anidempotency_keyreturns the same booking on a retry, as for any booking.- A course booking can't move (
bookings_moveanswers 409course_booking) and its guest can't change its time. Within your cancellation window they can cancel it, which cancels the whole course and refunds what was paid online. - Cancelling one time of a course with
sessions_cancelleaves its course bookings standing on the rest, with no refund in v1: each logsbooking.session_cancelled, and its guest gets one email naming the date. Removing the entry, or deleting the offering, with its bookings cancels each course booking whole, refunded as any cancel. - A course has no waitlist (409
waitlist_conflict). Its guests are on each of its times' rosters, marked course, and every time counts their seats. Reminders go for a course booking's first time only, Calendar feeds show a course booking at its first time, and resellers over OCTO don't see courses yet.