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.

Plain text

Event types

booking.heldA place is held while the guest decides or pays, until expires_at
booking.confirmedA booking is confirmed, from any surface and any path
booking.movedA booking moves to another time, in place; data.previous has the times it left
booking.cancelledA booking is cancelled
message.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 page
message.draftedA reply was drafted by AI for a conversation; nothing was sent
session.createdA class session is scheduled
session.cancelledA class session 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
customer.updatedA customer is added or edited
customer.deletedA customer's personal data is erased on request
org.updatedThe 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+<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 · then after=<next_after>
curl "https://daybag.io/api/v1/events?after=0" \
  -H "Authorization: Bearer daybag_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.
  • 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
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"
    ]
  }'
201response
{
  "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:

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:

Daybag-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 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.