OnworkDevelopers

Webhooks

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.

Setting up an endpoint

Endpoints are managed by a company administrator in Onwork (or via the admin API). Each endpoint has:

  • a destination URL — must be https and publicly reachable (private and local addresses are rejected);
  • a set of subscribed event types (see the catalog below);
  • a signing secret (whsec_...) — shown in the admin UI, rotatable at any time.

Event envelope

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.

Verifying signatures

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.

Responding and retries

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.

Event catalog

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.