workers first
IndustriesPricingSecurityFor ProvidersHow It Works
Sign InGet started freeBook a demo

Guides

  • Introduction
  • Authentication
  • Errors
  • Rate limits
  • Idempotency
  • Pagination
  • Scopes

API Reference

  • Organization
  • Members
  • Customers
  • Sites
  • Shifts
  • timesheets
  • invoices
  • Bookings
  • Webhooks

Webhooks

  • Events & signatures

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.

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.

Event types

member.createdA 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.updatedA member profile changed (contact details, capacity, status fields). Availability-calendar changes do not fire this event in v1.
Workers First

The all-in-one platform for organizations to manage their workforce, operations, and the people they serve.

support@keepworkersfirst.org

For Organizations

  • Book a Demo
  • Pricing
  • Security
  • Industries
  • Developers

For Providers

  • Join as Provider
  • Learn More

For Families

  • Create Account
  • Learn More

Company

  • About Us
  • Contact
© 2026 Workers First. All rights reserved.
Privacy PolicyTerms of ServiceAccessibility
member.verifiedA member passed verification (is_verified set to true), typically via POST /members/{id}/verify.
member.deactivatedA member was deactivated (is_active set to false) by an admin, the API, or self-service.
member.deletedA member row was permanently removed. The payload is a pre-delete snapshot of identifying fields.
customer.createdA customer (client company) was created.
customer.updatedA customer record changed.
customer_location.createdA site (customer location) was created.
customer_location.updatedA site changed, including activation/deactivation toggles.
shift.createdA shift was created (drafts included).
shift.updatedA shift changed — schedule, pay, positions, or fill-state changes from cancelled assignments.
shift.assignedA member was assigned to a shift, accepted an offer, or claimed an open call.
shift.completedA shift reached completed status.
shift.cancelledA shift was cancelled.
booking.createdA booking request was created.
booking.status_changedA booking moved between statuses (approved, active, completed, cancelled, …).
timesheet.createdA timesheet was generated for a shift assignment — at clock-out, by the missing-clock-out sweep, or manually by an admin.
timesheet.approvedA 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.disputedA customer disputed a pending timesheet; the organization resolves and resubmits it.
timesheet.voidedAn org admin voided a timesheet before approval (it will never be billed).
feedback.createdA customer submitted end-of-shift feedback (1–5 rating + optional comment) for a member.
invoice.createdAn invoice was generated from approved timesheets (draft unless auto-send is enabled).
invoice.sentAn invoice was finalized and sent to its recipient (journal entry posted at this transition).
invoice.paidAn invoice was marked paid (manual mark-paid — no payment processor in v1).
invoice.voidedAn invoice was voided; any posted journal entries were reversed and its source timesheets/expenses can be re-invoiced.
travel_expense.submittedA travel (mileage) expense was recorded — computed from driving distance or submitted by the member.
{
  "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"
  }
}
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);
}