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.

Plain text

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 it
booking.nudgedThe 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.confirmedA booking is confirmed, from any surface and any path; one made with a promo code carries promo_code and discount_cents
booking.movedA booking moves to another time, in place; data.previous has the times it left
booking.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 passes
booking.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 next
booking.paidA booking is marked paid by hand, for money taken outside daybooked; data.paid_at says when
booking.unpaidA booking marked paid by hand is marked unpaid again
booking.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 when
booking.no_showA confirmed booking is marked a no-show once its time has started; data.no_show_at says when
booking.attendance_clearedA check-in or a no-show is taken back: checked_in_at and no_show_at are both null again
booking.cancelledA booking is cancelled
booking.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 Stripe
message.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 page
message.draftedA reply was drafted by AI for a conversation; nothing was sent
session.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 bookings
offering.createdAn offering is added
offering.updatedAn offering changes or is paused
offering.deletedAn offering is deleted, after its upcoming bookings are cancelled
offering.hours_updatedWeekly hours are replaced
offering.calendar_updatedA calendar entry is added, changed or removed; data.entries is the offering's calendar after it
waiver.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.list
waiver.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_list
waitlist.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_ids
customer.updatedA customer is added or edited
customer.deletedA customer's personal data is erased on request
customers.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 rows
gift_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 message
gift_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 message
gift_card.adjustedA 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.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_email
promo_code.createdA promo code is added (promo_codes_create, or Settings → Codes): data is the code, with its discount, limit and dates
promo_code.updatedA promo code is switched on or off, or its limit, end or offering changes
promo_code.deletedA promo code is deleted: data is the code as it was; bookings made with it keep promo_code and discount_cents
org.updatedThe org's name, time zone, currency or messaging settings change
member.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 in
member.invite_revokedAn owner revokes an invite still waiting, or the person it's for declines it: data has id, email and role
member.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_id
member.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 name
member.removedAn 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+<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 · then after=<next_after>
curl "https://daybooked.io/api/v1/events?after=0" \
  -H "Authorization: Bearer daybooked_live_…"
200response
{
  "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.

curl
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"
    ]
  }'
201response
{
  "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:

POST · application/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=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>
  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 "<t>.<raw body>" 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.
node
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.