Webhooks let Crew tell your own systems when something happens, instead of you asking the
API over and over. Crew sends a signed JSON POST to your HTTPS address within seconds of
the event.
Adding a webhook
Super Users open Settings → Integrations → Webhooks → Add webhook and enter:
- the URL: a public
https://address. Addresses with a username or password, IP addresses and internal hostnames aren't accepted. - the Events to send (see below).
Crew creates a signing secret (starting with whsec_). Copy it into your receiving system; you
need it to check that deliveries really come from Crew. An organization can have up to 10 webhooks.
Click Test to send a ping event straight away, and Deliveries to see recent attempts, their
status code and how many tries they took.
Events
| Event | Sent when | data |
|---|---|---|
employee.created |
A member is added (by hand, import, automatically or by accepting an application). | The member (see below). |
employee.updated |
A member's profile or role changes. | The member. |
employee.deleted |
A member is deleted. | { "id", "name" }, or { "id", "erased": true } after a GDPR deletion. |
request.submitted |
A member submits a request. | { "id", "number", "type", "employeeId" } |
request.decided |
A request is approved or rejected. | { "id", "number", "type", "decision", "employeeId" } |
announcement.published |
An announcement is posted. | { "id", "title", "departmentId" } |
ping |
You click Test. | { "message": "Hello from Crew" } |
The member object has id, name, title, departmentId, unitId, managerId,
employmentStatus and discordId. Like the API, webhooks never include contact details, bios,
notes, disciplinary records or leave reasons.
What a delivery looks like
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Crew-Webhooks/1
X-Crew-Event: request.decided
X-Crew-Signature: t=1793030400,v1=5d41402abc4b2a76b9719d911017c592...
{
"id": "6a1f0d3e-7c2b-4f9a-8e5d-3b1c9a7e2f40",
"event": "request.decided",
"guildId": "123456789012345678",
"occurredAt": "2026-10-26T14:00:00.000Z",
"data": { "id": "b2e4f6a8-1c3d-4e5f-8a9b-0c1d2e3f4a5b", "number": "REQCREW000042", "type": "leave", "decision": "approved", "employeeId": "1001" }
}
id is unique per event: use it to ignore an event you've already handled (a retry can deliver the
same event twice).
Checking the signature
Always check that a delivery came from Crew before trusting it:
- Read
t(a Unix time in seconds) andv1from theX-Crew-Signatureheader. - Compute HMAC-SHA256 of
<t>.<raw request body>using your signing secret, as lowercase hex. - Compare it with
v1using a constant-time comparison. - Reject the delivery if
tis more than about 5 minutes old, so a captured request can't be replayed later.
Use the raw body exactly as received, before any JSON parsing.
// Node.js (Express): app.post("/crew-webhook", express.raw({ type: "application/json" }), handler)
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyCrewWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // too old
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "hex");
const wanted = Buffer.from(expected, "hex");
return given.length === wanted.length && timingSafeEqual(given, wanted);
}
Responding, retries and pausing
- Answer with any 2xx status within 10 seconds. Do slow work after answering.
- Redirects aren't followed, and count as failures.
- A failed delivery is retried after about 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours: six attempts over roughly 15 hours. After that it's abandoned.
- After 10 failed deliveries in a row, the webhook is paused. Fix your endpoint, then turn it back on in the webhook list.
Deliveries are kept for 7 days.
Warning: Webhooks send organization data to a system you control, and your organization is responsible for it. Treat the signing secret like a password. If it leaks, delete the webhook and add it again to get a new secret.