Webhooks

Webhooks push your organization's volunteer and shift activity to a URL you control, so you can sync a CRM, trigger an automation, or log to a spreadsheet in real time. Configure one destination URL under Settings → Webhooks. Webhooks are available on the Engage plan.

Each endpoint only ever receives events for its own organization.

The request

Every event is an HTTP POST with a JSON body in this envelope:

{
  "id": "d6b1a3e2-8c47-4f0a-9b21-2f9a7e5c1d80",
  "type": "signup.created",
  "created_at": "2026-07-10T14:32:00.000Z",
  "organization_id": "8a7c1f34-5e2b-4c9d-a1f6-0b3e7d9c2a11",
  "data": { /* event-specific, see below */ }
}

Along with these headers:

X-Webhook-EventThe event type, e.g. signup.created.
X-Webhook-DeliveryUnique delivery id (equals the body's id). De-duplicate on this.
X-Webhook-Signaturesha256=<hex> — HMAC-SHA256 of the raw body, keyed by your signing secret.
Content-Typeapplication/json

Verifying the signature

Recompute the signature over the raw request body using your endpoint's signing secret (find and rotate it in Settings) and compare with a constant-time check. Reject anything that doesn't match.

import crypto from "crypto";
import express from "express";

const app = express();

// IMPORTANT: verify against the RAW body, before JSON parsing.
app.post(
  "/webhooks/volunteers",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const signature = req.header("X-Webhook-Signature") ?? "";
    const expected =
      "sha256=" +
      crypto
        .createHmac("sha256", process.env.VSM_WEBHOOK_SECRET)
        .update(req.body) // req.body is a Buffer here
        .digest("hex");

    const valid =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!valid) return res.status(401).send("bad signature");

    const event = JSON.parse(req.body.toString("utf8"));
    // Delivery is at-least-once — de-duplicate on event.id before acting.
    handleEvent(event);

    res.status(200).send("ok"); // 2xx = delivered; anything else is retried
  },
);

Delivery & retries

Events

volunteer.createdA volunteer record is first created for your organization.
signup.createdA volunteer signs up and is confirmed for a shift.
signup.waitlistedA volunteer signs up for a full shift and joins its waitlist.
signup.promotedA waitlisted volunteer moves into a confirmed spot.
signup.canceledA shift signup is canceled by anyone (volunteer, coordinator, or system).
volunteer.updatedA volunteer's name, contact info, or communication preferences change.
volunteer.removedA volunteer leaves your organization or is deleted.
volunteer.checked_inA volunteer is checked in to a shift.
volunteer.checked_outA volunteer is checked out (clocked out) of a shift.

Volunteer events

volunteer.created — a person is added to your org:

"data": {
  "volunteer": {
    "id": "3f2a...",
    "name": "Jordan Lee",
    "email": "jordan@example.org",
    "phone": "+15551234567",
    "created_at": "2026-07-10T14:32:00.000Z"
  }
}

volunteer.updated — details or preferences change:

"data": {
  "volunteer": {
    "id": "3f2a...",
    "name": "Jordan Lee",
    "email": "jordan@example.org",
    "phone": "+15551234567",
    "email_consent": true,
    "sms_consent": false
  }
}

volunteer.removed — they leave or are deleted:

"data": {
  "volunteer": { "id": "3f2a...", "name": "Jordan Lee", "email": "jordan@example.org" },
  "reason": "left"   // "left" (self-service) or "deleted" (coordinator)
}

Signup events

signup.created, signup.waitlisted, signup.promoted, signup.canceled, volunteer.checked_in, and volunteer.checked_out all share this shape — the type tells you what happened and the relevant fields are populated (e.g. waitlist_position on a waitlisted signup, checked_in_at on a check-in):

"data": {
  "signup": {
    "id": "9c4d...",
    "status": "confirmed",        // confirmed | waitlist | canceled
    "waitlist_position": null,    // set when status is "waitlist"
    "canceled_by": null,          // "volunteer" | "coordinator" | "system"
    "cancel_reason": null,
    "checked_in_at": null,        // ISO timestamp on volunteer.checked_in
    "checked_out_at": null        // ISO timestamp on volunteer.checked_out
  },
  "volunteer": { "id": "3f2a...", "name": "Jordan Lee", "email": "jordan@example.org" },
  "shift": {
    "id": "1b8e...",
    "title": "Saturday food bank",
    "start_time": "2026-07-18T13:00:00.000Z",
    "end_time": "2026-07-18T16:00:00.000Z",
    "program_name": "Weekend meals"
  }
}

Ready to start? Add your endpoint in Settings and use “Send test event” to see your first delivery.