# Events and webhooks Every change in daybag writes an event with an increasing id. Poll `GET /api/v1/events?after=` to follow along, or add an HTTPS endpoint with `webhooks_create`: daybag POSTs each event as JSON, signed in a `Daybag-Signature` header, in order and at least once, and retries until your endpoint answers 2xx. URL: https://daybag.io/docs/webhooks ## Event types - booking.held: A place is held while the guest decides or pays, until expires_at - booking.confirmed: A booking is confirmed, from any surface and any path - booking.moved: A booking moves to another time, in place; data.previous has the times it left - booking.cancelled: A booking is cancelled - message.sent: The outfitter 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 - session.cancelled: A class session 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 - customer.updated: A customer is added or edited - customer.deleted: A customer's personal data is erased on request - org.updated: The org's name, time zone, currency or messaging settings change 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. The outfitter's messages reach the guest with a Reply-To of `reply+@daybag.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 outfitter 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://daybag.io/api/v1/events?after=0" \ -H "Authorization: Bearer daybag_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. - 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`. The signing secret comes back once. ```bash curl -X POST https://daybag.io/api/v1/webhooks \ -H "Authorization: Bearer daybag_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/daybag", "events": [ "booking.confirmed", "booking.cancelled" ] }' ``` Response 201: ```json { "id": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d", "url": "https://example.com/hooks/daybag", "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: Daybag-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 Daybag-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. - A new endpoint starts with the next event; poll `events_list` for history. - `webhooks_list` shows each endpoint's last status and error.