# Events and webhooks Every change in daybooked writes an event with an increasing id. Poll `GET /api/v1/events?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. URL: https://daybooked.io/docs/webhooks ## Event types - booking.held: A 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 it - booking.nudged: The guest of a hold that lapsed unpaid at Checkout is emailed a link to finish booking (bookings_nudge), once; data.nudged_at says when - booking.confirmed: A booking is confirmed, from any surface and any path; one made with a promo code carries promo_code and discount_cents - booking.moved: A booking moves to another time, in place; data.previous has the times it left - booking.called_off: Its 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 passes - booking.session_cancelled: One 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 next - booking.paid: A booking is marked paid by hand, for money taken outside daybooked; data.paid_at says when - booking.unpaid: A booking marked paid by hand is marked unpaid again - booking.checked_in: A 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 when - booking.no_show: A confirmed booking is marked a no-show once its time has started; data.no_show_at says when - booking.attendance_cleared: A check-in or a no-show is taken back: checked_in_at and no_show_at are both null again - booking.cancelled: A booking is cancelled - booking.refunded: A payment made online is refunded, in full or the part chosen on cancelling (refund_cents): Stripe confirmed it (refunded_at) - booking.refund_failed: A refund failed for good and nothing will try again (refund_error says why); the owner is emailed to refund it in Stripe - message.sent: The business writes to a guest from the dashboard, the API or MCP, or the org auto-acknowledges a guest (channel system) - message.received: A guest writes: on their booking page, by email or text, or with a question before booking on the hosted booking page - message.drafted: A reply was drafted by AI for a conversation; nothing was sent - session.created: A class session is scheduled on its own (a calendar entry's times come with offering.calendar_updated) - session.cancelled: A time on a calendar is cancelled, with its bookings - offering.created: An offering is added - offering.updated: An offering changes or is paused - offering.deleted: An offering is deleted, after its upcoming bookings are cancelled - offering.hours_updated: Weekly hours are replaced - offering.calendar_updated: A calendar entry is added, changed or removed; data.entries is the offering's calendar after it - waiver.signed: Someone 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.list - waiver.updated: The waiver's text is saved as a new version, or removed (body null) - waitlist.joined: A 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_list - waitlist.left: A 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.opened: A 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_ids - customer.updated: A customer is added or edited - customer.deleted: A customer's personal data is erased on request - customers.imported: A 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 rows - gift_card.purchased: A 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 message - gift_card.issued: You 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 message - gift_card.adjusted: A gift card's balance was fixed (gift_cards_adjust): data has gift_card_id, cents (signed), balance, note (your reason) and for_email - gift_card.refunded: A 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_email - promo_code.created: A promo code is added (promo_codes_create, or Settings → Codes): data is the code, with its discount, limit and dates - promo_code.updated: A promo code is switched on or off, or its limit, end or offering changes - promo_code.deleted: A promo code is deleted: data is the code as it was; bookings made with it keep promo_code and discount_cents - org.updated: The org's name, time zone, currency or messaging settings change - member.invited: An 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 in - member.invite_revoked: An owner revokes an invite still waiting, or the person it's for declines it: data has id, email and role - member.joined: Someone 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_id - member.updated: An 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 name - member.removed: An owner removes someone from the team: data has user_id, email and the role they had A 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+@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. ```bash curl "https://daybooked.io/api/v1/events?after=0" \ -H "Authorization: Bearer daybooked_live_…" ``` Response 200: ```json { "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 (`limit` up to 200). Filter with `type`, such as `type=booking.confirmed`. - Pass `next_after` back as `after` to get what happened next; `after=0` starts from the beginning. - `order=desc` serves the newest first, for a look at what just happened rather than paging; `next_after` is 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. ```bash 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" ] }' ``` Response 201: ```json { "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: ```json { "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=,v1=.")> 1. Keep the raw request body; verify before you parse it. 2. Split the header at the commas: t is a unix time, v1 the signature. 3. HMAC-SHA256 the string "." with your webhook secret, hex encoded, and compare it to v1 in constant time. 4. Reject a t more than five minutes away, so a captured request can't be replayed. ```js 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_list` shows it with `active: false` and why in `last_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_list` for history. - `webhooks_list` shows each endpoint's last status and error.