Webhooks
Events & signatures
Webhooks push events to your server as they happen. Register an endpoint (a public HTTPS URL) in the Developer area or via the API, choose the events to subscribe to, and we POST each event to your URL.
Each delivery is a JSON envelope: `{ "id", "type", "created_at", "mode", "data" }`. The `data` object uses the same resource shapes as the REST API. The `id` is stable across retries — use it to dedupe.
Deliveries are retried with backoff for about 24 hours (up to 8 attempts). Respond with any 2xx status to acknowledge; a `410 Gone` tells us to stop immediately.
Event envelope
Every delivery has this shape. The `data` object matches the REST resource for that event.
{
"id": "evt_9f1b6e9e11114222",
"type": "member.created",
"created_at": "2026-07-07T10:00:00+00:00",
"mode": "live",
"data": {
"id": "a1b2c3d4-1111-4a2b-8c3d-e4f5a6b7c8d9",
"business_name": "Example Care Services",
"display_name": null,
"description": "Experienced overnight-availability team member.",
"status": "draft",
"is_active": true,
"is_verified": false,
"capacity": 6,
"age_groups": [
"infant",
"toddler"
],
"employment_relationship": "contractor",
"contract_type": "casual",
"contract_start_date": "2026-02-01",
"contract_end_date": null,
"contact": {
"email": "member@example.com",
"phone": null
},
"address": {
"line1": null,
"line2": null,
"city": "Portland",
"state": "OR",
"postal_code": null,
"country": "US"
},
"created_at": "2026-02-01T17:20:00+00:00",
"updated_at": "2026-06-28T09:12:00+00:00",
"source": "signup"
}
}Verifying signatures
Every request is signed. The `X-Webhook-Signature` header is `t=<unix seconds>,v1=<hex hmac_sha256(t + "." + rawBody)>`, keyed with your endpoint’s signing secret (`whsec_…`).
Verify it against the raw request body before trusting a payload, and reject timestamps outside a tolerance (5 minutes is typical). During a secret roll, the header may carry multiple `v1=` signatures — accept the payload if any of them matches.
const crypto = require('crypto');
// Verify an incoming webhook. Use the raw request body (not re-serialized JSON).
function verifyWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
const parts = signatureHeader.split(',').map((p) => p.split('='));
const t = Number(parts.find(([k]) => k === 't')?.[1]);
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v);
if (!t || sigs.length === 0) throw new Error('Malformed signature header');
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) throw new Error('Timestamp outside tolerance');
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const ok = sigs.some(
(s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s, 'hex'), Buffer.from(expected, 'hex'))
);
if (!ok) throw new Error('Signature mismatch');
return JSON.parse(rawBody);
}Event types
member.created | A member was created — via public signup (source "signup"), a portal invitation, the hiring pipeline, or the API. Subscribe to this to verify members who sign up publicly. |
member.updated | A member profile changed (contact details, capacity, status fields). Availability-calendar changes do not fire this event in v1. |
member.verified | A member passed verification (is_verified set to true), typically via POST /members/{id}/verify. |
member.deactivated | A member was deactivated (is_active set to false) by an admin, the API, or self-service. |
member.deleted | A member row was permanently removed. The payload is a pre-delete snapshot of identifying fields. |
customer.created | A customer (client company) was created. |
customer.updated | A customer record changed. |
customer_location.created | A site (customer location) was created. |
customer_location.updated | A site changed, including activation/deactivation toggles. |
shift.created | A shift was created (drafts included). |
shift.updated | A shift changed — schedule, pay, positions, or fill-state changes from cancelled assignments. |
shift.assigned | A member was assigned to a shift, accepted an offer, or claimed an open call. |
shift.completed | A shift reached completed status. |
shift.cancelled | A shift was cancelled. |
booking.created | A booking request was created. |
booking.status_changed | A booking moved between statuses (approved, active, completed, cancelled, …). |
timesheet.created | A timesheet was generated for a shift assignment — at clock-out, by the missing-clock-out sweep, or manually by an admin. |
timesheet.approved | A timesheet was approved — by the customer (portal or email link), automatically (customer opt-out or review window elapsed), or by an org admin. Approval triggers invoice generation. |
timesheet.disputed | A customer disputed a pending timesheet; the organization resolves and resubmits it. |
timesheet.voided | An org admin voided a timesheet before approval (it will never be billed). |
feedback.created | A customer submitted end-of-shift feedback (1–5 rating + optional comment) for a member. |
invoice.created | An invoice was generated from approved timesheets (draft unless auto-send is enabled). |
invoice.sent | An invoice was finalized and sent to its recipient (journal entry posted at this transition). |
invoice.paid | An invoice was marked paid (manual mark-paid — no payment processor in v1). |
invoice.voided | An invoice was voided; any posted journal entries were reversed and its source timesheets/expenses can be re-invoiced. |
travel_expense.submitted | A travel (mileage) expense was recorded — computed from driving distance or submitted by the member. |