Webhooks push events from Onwork to your system the moment they happen — no polling. Onwork POSTs a signed JSON payload to your HTTPS endpoint for every event type you subscribe to.
Endpoints are managed by a company administrator in Onwork (or via the admin API). Each endpoint has:
https and publicly reachable (private and
local addresses are rejected);whsec_...) — shown in the admin UI, rotatable at
any time.Every delivery is a POST with this JSON body:
{
"id": "evt_9f2c1a7e0b4d3a5c6e8f0a1b",
"type": "shifts.created",
"api_version": "v1",
"created_at": "2026-07-20T10:30:00+00:00",
"company_id": "65f000000000000000000001",
"data": { "...": "the resource, same shape as the corresponding GET" }
}
data is rendered through the same resource model as the REST API (e.g. a
shifts.created event carries exactly a Shift resource) — a webhook never
exposes more than the corresponding GET endpoint.
Headers on every delivery:
| Header | Meaning |
|---|---|
X-Onwork-Event | The event type (e.g. shifts.created). |
X-Onwork-Event-Id | Stable event id — the same across retries and endpoints. |
X-Onwork-Delivery-Id | Unique per delivery attempt chain. |
X-Onwork-Signature | HMAC signature — see below. |
The signature header has the form t=<unix_ts>,v1=<hex> where
hex = HMAC_SHA256(secret, "<t>.<raw_body>"). Always verify before trusting
a delivery:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs(time.time() - t) > tolerance:
return False # replay protection
expected = hmac.new(secret.encode(), f"{t}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)
Use the raw request body bytes — re-serializing the JSON breaks the digest.
Respond with any 2xx within 10 seconds — do heavy processing asynchronously. Anything else (including timeouts) schedules a retry with exponential backoff: ~1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours. After 6 failed attempts the delivery is marked dead.
An endpoint that accumulates 5 consecutive dead deliveries is automatically disabled and the company administrators are notified; it can be re-enabled from the settings after fixing the receiver.
Deliveries can arrive more than once (retries after a lost response) and
out of order — deduplicate on X-Onwork-Event-Id and treat the payload
as a snapshot, not a diff.
Events mirror the published resources: <resource>.<created|updated|deleted>.
Like scopes, each event belongs to a platform module — subscriptions require
the module to be active, and deactivating a module silences its events.
| Event | Module |
|---|---|
company.updated | Core |
users.created / users.updated / users.deleted | Core |
employees.created / employees.updated / employees.deleted | Core |
projects.created / projects.updated / projects.deleted | Projects |
shifts.created / shifts.updated / shifts.deleted | Projects |
absences.created / absences.updated / absences.deleted | Projects |
expenses.created / expenses.updated / expenses.deleted | Projects |
*.deleted fires on soft-deletion (the platform's standard delete). The
catalog grows together with the resource catalog — see the
changelog.