Events and webhooks
Every change in daybag 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: 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.
Event types
booking.heldA place is held while the guest decides or pays, until expires_atbooking.confirmedA booking is confirmed, from any surface and any pathbooking.movedA booking moves to another time, in place; data.previous has the times it leftbooking.cancelledA booking is cancelledmessage.sentThe outfitter 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 scheduledsession.cancelledA class session 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 replacedcustomer.updatedA customer is added or editedcustomer.deletedA customer's personal data is erased on requestorg.updatedThe org's name, time zone, currency or messaging settings changeA 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+<token>@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.
curl "https://daybag.io/api/v1/events?after=0" \
-H "Authorization: Bearer daybag_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. - 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.
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"
]
}'{
"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:
{
"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=<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 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_listfor history. webhooks_listshows each endpoint's last status and error.