Webhooks
Webhooks push an event to your own URL the moment something happens in your workspace, so you never have to poll for changes. Add an endpoint under Developer > Webhooks, choose the events you want or pick All events, and each matching change sends a signed POST.
Every event WorkBOS can send is listed below. It is the same list WorkBOS itself holds, and a build check fails when the platform starts sending a type this table does not name, so it cannot quietly fall behind again: it listed eight events while nineteen were being sent.
| Event | Fires when | Can contain text from outside your workspace |
|---|---|---|
| task.created | A task is created | No |
| project.created | A project is created | No |
| client.created | A client is created | No |
| deal.created | A deal is created | No |
| deal.stage_changed | A deal moves to a different stage | No |
| deal.won | A deal is marked Won | No |
| invoice.created | An invoice is created | No |
| invoice.paid | An invoice is marked paid | No |
| tag.added | A tag is added to a record | No |
| lead.became_hot | A lead crosses the hot threshold | No |
| form.submitted | Someone submits a published form | Yes |
| survey.completed | Someone completes a survey | Yes |
| sms.received | An inbound text message arrives | Yes |
| voicemail.received | A voicemail is left, with its transcript | Yes |
| call.completed | A call ends | Yes |
| call.missed | A call is missed | Yes |
| call.ai_intake | The AI receptionist takes an enquiry | Yes |
| chief.flags | The assistant flags something for attention | No |
| chief.dream.completed | An overnight assistant run finishes | No |
| webhook.secret_rotated | An endpoint signing secret is rotated | No |
Choose your events rather than taking All events. “All events” means every event that exists now AND every one added later, so an endpoint set up for deals and invoices started receiving voicemail transcripts and inbound text messages the day those events shipped: words a member of the public typed or spoke, arriving in whatever channel you pointed at us. New endpoints now start with the eight business events above; the last column tells you which of the rest carry outside text before you add them.
- X-WorkBOS-Event names the event that fired, using the same names as the table above.
- X-WorkBOS-Signature-V1 is what to verify: t=<unix seconds>,v1=<lowercase hex>. Check t is within five minutes of your clock, then recompute HMAC-SHA256 over the exact string "<t>.<raw body>" with your endpoint secret and compare it to the v1 half. Because the timestamp is signed, a delivery somebody captured yesterday stops verifying.
- X-WorkBOS-Signature is the form integrations written before this used, and is still sent, unchanged: "sha256=" followed by the HMAC-SHA256 of the raw body alone. It verifies the same bytes for ever, so treat it as a fallback while you move to the timestamped one. An endpoint with no secret set is never delivered to, so neither header is ever empty.
- X-WorkBOS-Timestamp repeats the t value for readability. X-WorkBOS-Delivery repeats the delivery id, which is also inside the body as workbos_delivery_id. Read the body one, because the body is signed and headers are not. A delivery that fails is retried automatically with backoff, up to six attempts, so the same id can arrive more than once: treat it as an idempotency key.
Verify the raw bytes of the body, before any JSON.parse or framework body-parsing reshapes them: recompute the signature over exactly what you received and compare it to X-WorkBOS-Signature. Slack, Microsoft Teams and Discord endpoints are the exception: they receive a pre-formatted chat message instead of raw JSON, so there is nothing to verify by hand.
const crypto = require('crypto');
function isValidWorkBOSSignature(rawBody, headerValue, endpointSecret) {
const expected = 'sha256=' + crypto.createHmac('sha256', endpointSecret).update(rawBody).digest('hex');
const a = Buffer.from(headerValue || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// rawBody must be the unparsed request body, for example from express.raw().
app.post('/webhooks/workbos', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-WorkBOS-Signature') || '';
if (!isValidWorkBOSSignature(req.body, header, process.env.WORKBOS_ENDPOINT_SECRET)) {
return res.status(401).send('invalid signature');
}
res.status(200).send('ok');
});Return any 2xx status to acknowledge a delivery. A failed or timed-out delivery retries automatically with exponential backoff, up to six attempts, and delivery status and attempt history are visible under Developer > Webhooks. If an endpoint fails every attempt on twenty deliveries inside a day, WorkBOS switches it off and tells the workspace owners why, rather than retrying a dead address for ever. Fix the address and switch it back on from the same page.
Endpoints must be https. A plain http address is refused, because every deal, invoice, voicemail and text-message payload would otherwise travel in the clear where anything on the network path can read it.