WorkscopeAPI Open Workscope

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

EventWhen
booking.createdA booking is made (in the app, by an import or through the API)
booking.updatedA booking changes (its time, who, price, notes and so on), not its status
booking.status_changedA booking moves status: open, active, complete, signed off or failed. Has previousStatus.
booking.deletedA booking is deleted. data is what it was: id, clientId, projectId, teamMemberId, date, status.
timesheet.submittedA timesheet is sent in to be signed off
absence.approvedTime off is approved (or added already approved)
absence.cancelledTime off is cancelled
invoice.issuedAn invoice is issued
invoice.paidAn invoice is marked paid
quote.acceptedA client accepts a quote
enquiry.receivedA new enquiry arrives
client.createdA client is added
client.updatedA client changes (including being archived)
pingOnly 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.