Events and webhooks
Every change in daybooked writes an event with an increasing id. Poll GET /api/v1/events?after=<next_after> to follow along, or add an HTTPS endpoint with webhooks_create: daybooked POSTs each event as JSON, signed in a Daybooked-Signature header, in order and at least once, and retries until your endpoint answers 2xx.
Event types
booking.heldA place is held while the guest decides or pays, until expires_at; with source assistant, an assistant held it for the guest over the business's own MCP (/api/mcp/book/{slug}), and the guest is emailed to confirm itbooking.nudgedThe guest of a hold that lapsed unpaid at Checkout is emailed a link to finish booking (bookings_nudge), once; data.nudged_at says whenbooking.confirmedA booking is confirmed, from any surface and any path; one made with a promo code carries promo_code and discount_centsbooking.movedA booking moves to another time, in place; data.previous has the times it leftbooking.called_offIts time is called off with sessions_cancel and guests: "choose": the booking stays confirmed while its guest picks another time or takes the refund, until data.choose_by; data.note has your words, and data.room is false when no other time had room for the party. A move ends it, or booking.cancelled once choose_by passesbooking.session_cancelledOne time of a course is called off (sessions_cancel): the course booking stays confirmed on its other times, with no refund for the time lost; data.session has the time called off and data.note your words. One whose first time it was now starts at the nextbooking.paidA booking is marked paid by hand, for money taken outside daybooked; data.paid_at says whenbooking.unpaidA booking marked paid by hand is marked unpaid againbooking.checked_inA guest is checked in at the door (a rental's guest: picked up), on the roster or with bookings_set_attendance; data.checked_in_at says whenbooking.no_showA confirmed booking is marked a no-show once its time has started; data.no_show_at says whenbooking.attendance_clearedA check-in or a no-show is taken back: checked_in_at and no_show_at are both null againbooking.cancelledA booking is cancelledbooking.refundedA payment made online is refunded, in full or the part chosen on cancelling (refund_cents): Stripe confirmed it (refunded_at)booking.refund_failedA refund failed for good and nothing will try again (refund_error says why); the owner is emailed to refund it in Stripemessage.sentThe business writes to a guest from the dashboard, the API or MCP, or the org auto-acknowledges a guest (channel system)message.receivedA guest writes: on their booking page, by email or text, or with a question before booking on the hosted booking pagemessage.draftedA reply was drafted by AI for a conversation; nothing was sentsession.createdA class session is scheduled on its own (a calendar entry's times come with offering.calendar_updated)session.cancelledA time on a calendar is cancelled, with its bookingsoffering.createdAn offering is addedoffering.updatedAn offering changes or is pausedoffering.deletedAn offering is deleted, after its upcoming bookings are cancelledoffering.hours_updatedWeekly hours are replacedoffering.calendar_updatedA calendar entry is added, changed or removed; data.entries is the offering's calendar after itwaiver.signedSomeone signs the waiver for a booking, the booker as they book (one event for them and one for each child) or anyone from its signing link: data has booking_id, reference, signature_id, and signed and needed counts; who signed is in waiver_signatures.listwaiver.updatedThe waiver's text is saved as a new version, or removed (body null)waitlist.joinedA guest joins a full time's waitlist, or their places change: data has id, session_id, offering_id, customer_id, starts_at and places; who it is is in waitlist_listwaitlist.leftA guest comes off a waitlist: data.reason is left (by their own link), removed, booked (they booked that time), called_off (the time was cancelled) or closed (the waitlist was switched off)waitlist.openedA place opened at a time and everyone waiting whose party fits was emailed at once: data has session_id, offering_id, starts_at, remaining, told and entry_idscustomer.updatedA customer is added or editedcustomer.deletedA customer's personal data is erased on requestcustomers.importedA spreadsheet of customers is imported on the Customers page, 500 rows at a time, one event each (a big file writes several): data has created, updated (already a customer: each keeps their name and phone and gets only what was missing) and skipped (rows without a valid email), never the rowsgift_card.purchasedA guest bought a gift card on your booking page and Stripe took the payment: data has gift_card_id, amount_cents, currency and for_email, never the code or the messagegift_card.issuedYou issued a gift card (gift_cards_issue, or Settings → Gift cards): data has gift_card_id, amount_cents, currency and for_email, never the code or the messagegift_card.adjustedA gift card's balance was fixed (gift_cards_adjust): data has gift_card_id, cents (signed), balance, note (your reason) and for_emailgift_card.refundedA booking a gift card paid for was cancelled, and its part went back on the card: data has gift_card_id, booking_id, cents and for_emailpromo_code.createdA promo code is added (promo_codes_create, or Settings → Codes): data is the code, with its discount, limit and datespromo_code.updatedA promo code is switched on or off, or its limit, end or offering changespromo_code.deletedA promo code is deleted: data is the code as it was; bookings made with it keep promo_code and discount_centsorg.updatedThe org's name, time zone, currency or messaging settings changemember.invitedAn owner invites someone to the team by email in Settings → Team: data has id, email, role and invited_by (their user id); the invite waits until that address signs inmember.invite_revokedAn owner revokes an invite still waiting, or the person it's for declines it: data has id, email and rolemember.joinedSomeone invited joins the team as they sign in with that address, or with Join when they work in another business already: data has user_id, email, role and invite_idmember.updatedAn owner makes a staff member an owner: data has user_id, email and role. A member's name changes (Settings → Team): data has user_id, email and namemember.removedAn owner removes someone from the team: data has user_id, email and the role they hadA paid booking is held first (booking.held) and confirmed when the payment lands; a free one confirms at once. Each event's data is the resource after the change: the booking, session, offering, customer or org.
Guests can answer by email too. Messages to a guest carry a Reply-To of reply+<token>@daybooked.io, one address per conversation, so a reply joins the thread without the quoted email below it, as message.received with channel email, and the owner is emailed as for any guest message. Mail to that address from anyone but the guest is dropped. Messages to guests get delivered_at when their mail server takes the email, or failed_at when it bounces or is marked as spam.
Poll
events_list pages through everything that happened, with after as the cursor.
curl "https://daybooked.io/api/v1/events?after=0" \
-H "Authorization: Bearer daybooked_live_…"{
"data": [
{
"id": 42,
"type": "booking.confirmed",
"data": {
"id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
"reference": "0GWYRZ2V",
"status": "confirmed"
},
"created_at": "2026-12-01T17:04:11+00:00"
}
],
"next_after": 42
}- Oldest first, up to 50 a call (
limitup to 200). Filter withtype, such astype=booking.confirmed. - Pass
next_afterback asafterto get what happened next;after=0starts from the beginning. order=descserves the newest first, for a look at what just happened rather than paging;next_afteris still the newest event served.- An event is served once it's 2 seconds old, so a cursor never skips one still being written.
Webhooks
Add an HTTPS endpoint with webhooks_create, up to 50 active per org. The signing secret comes back once. One more is refused with 429 too_many_webhooks: a limit, not a rate limit, so no Retry-After; remove one first.
curl -X POST https://daybooked.io/api/v1/webhooks \
-H "Authorization: Bearer daybooked_live_…" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/daybooked",
"events": [
"booking.confirmed",
"booking.cancelled"
]
}'{
"id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
"url": "https://example.com/hooks/daybooked",
"events": [
"booking.confirmed",
"booking.cancelled"
],
"active": true,
"secret": "whsec_…"
}Each event then arrives as a JSON POST:
{
"id": 42,
"type": "booking.confirmed",
"created_at": "2026-12-01T17:04:11+00:00",
"data": {
"id": "1e2d3c4b-5a69-4788-9a6b-5c4d3e2f1a0b",
"reference": "0GWYRZ2V",
"status": "confirmed"
}
}Verify
Every delivery is signed in this header:
Daybooked-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>- Keep the raw request body; verify before you parse it.
- Split the header at the commas: t is a unix time, v1 the signature.
- HMAC-SHA256 the string "<t>.<raw body>" with your webhook secret, hex encoded, and compare it to v1 in constant time.
- Reject a t more than five minutes away, so a captured request can't be replayed.
import { createHmac, timingSafeEqual } from "node:crypto";
// body: the raw request body · header: the Daybooked-Signature header
function verify(secret, body, header) {
const { t, v1 = "" } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // missing, stale or replayed
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}Delivery and retries
- Each endpoint gets the events it subscribes to, oldest first, one request at a time.
- A 2xx moves the endpoint's cursor past the event. Anything else stops delivery there: an error status, a redirect, or no answer within 5 seconds.
- A stopped endpoint picks up from the first event it missed: with its next event, and every 5 minutes, until it answers 2xx. Up to 50 events go out per run.
- Delivery is at least once: dedupe on the event id.
- An endpoint that answers 410 Gone is switched off: it has said it wants no more, as Zapier does for a Zap that's off.
webhooks_listshows it withactive: falseand why inlast_error, out of the count of 50, until you delete it. - Revoking the API key that made a webhook (
api_key_id) switches it off at once, as a 410 does: a leaked key's webhooks stop with it. - A new endpoint starts with the next event; poll
events_listfor history. webhooks_listshows each endpoint's last status and error.