Webhooks
Webhooks let Workscope tell your system when something happens, as it happens. You give Workscope an address, choose the events you want, and Workscope sends each one there as a POST with a JSON body.
Setting one up
In Workscope, go to Settings › Webhooks and choose Add a webhook. Give the address (it must start https://) and tick the events to send, or All events. You'll see its signing secret once: copy it into your system. Use Send a test to check it works.
You can also manage webhooks with the API, using a key that isn't read only: POST /v1/webhook-endpoints and the rest under Webhooks in the reference.
Events
| Event | When |
|---|---|
booking.created | A booking is made (in the app, by an import or through the API) |
booking.updated | A booking changes (its time, who, price, notes and so on), not its status |
booking.status_changed | A booking moves status: open, active, complete, signed off or failed. Has previousStatus. |
booking.deleted | A booking is deleted. data is what it was: id, clientId, projectId, teamMemberId, date, status. |
timesheet.submitted | A timesheet is sent in to be signed off |
absence.approved | Time off is approved (or added already approved) |
absence.cancelled | Time off is cancelled |
invoice.issued | An invoice is issued |
invoice.paid | An invoice is marked paid |
quote.accepted | A client accepts a quote |
enquiry.received | A new enquiry arrives |
client.created | A client is added |
client.updated | A client changes (including being archived) |
ping | Only from Send a test |
The list is also at GET /v1/webhook-endpoints/events.
What you receive
POST /your/address
Content-Type: application/json
User-Agent: Workscope-Webhooks/1
Workscope-Event: booking.status_changed
Workscope-Delivery: 7c0e… (this delivery)
Workscope-Signature: t=1793950000,v1=5d41402abc4b2a76…
{
"id": "e3b0c442-…", // the event: the same on every retry
"event": "booking.status_changed",
"createdAt": "2026-11-06T09:12:03.000Z",
"organizationId": "70e6872b-…",
"previousStatus": "active", // status_changed only
"data": { …the booking, exactly as GET /v1/bookings/{id} answers… }
}
data is the record as its GET answers at the moment the event is sent out (within a few seconds), seen with the role of the person who added the webhook. Several saves of one thing in one go send one event.
Checking it's from Workscope
Each request is signed with your secret. Workscope-Signature has a time t (Unix seconds) and v1, the hex HMAC-SHA256 of t, a full stop, and the raw body. Work it out yourself and compare. Refuse requests whose t is more than 5 minutes old.
Node:
import { createHmac, timingSafeEqual } from 'node:crypto'
// rawBody: the request body exactly as received (a string), not re-encoded JSON
function fromWorkscope(rawBody, header, secret) {
const t = /t=(\d+)/.exec(header)?.[1]
const v1 = /v1=([0-9a-f]+)/.exec(header)?.[1]
if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
return expected.length === v1.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
}
PHP:
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_WORKSCOPE_SIGNATURE'] ?? '';
preg_match('/t=(\d+)/', $header, $t);
preg_match('/v1=([0-9a-f]+)/', $header, $v1);
$ok = $t && $v1
&& abs(time() - (int) $t[1]) <= 300
&& hash_equals(hash_hmac('sha256', $t[1] . '.' . $raw, $secret), $v1[1]);
if (!$ok) { http_response_code(400); exit; }
Answering
Answer with any 2xx within 10 seconds, then do the work. Anything else, no answer, or a redirect counts as a failure (redirects are not followed: give the final address). The same event can arrive more than once, so use its id to skip ones you've had.
Retries and switching off
A failed delivery is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours, then marked failed. If nothing sent to an address gets through for 3 days, Workscope switches the webhook off and tells the company's owners and admins. Switch it on again once it's fixed.
Settings › Webhooks shows each delivery for 30 days, what was sent, and the answer, with Send again. The API has the same: GET /v1/webhook-endpoints/{id}/deliveries and POST …/deliveries/{deliveryId}/redeliver.
Keeping it safe
- Always check the signature. The address alone proves nothing.
- If the secret may have leaked, choose New secret. The old one stops at once.
- Addresses must be on the public internet over https; private and local addresses are refused.