DuckdayDevelopers

Webhooks

Webhooks push events from Duckday to your system the moment they happen — no polling. Duckday 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 Duckday (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. Re-enabling resets the failure count.

Ask them to set a technical contact on the endpoint: an email address warned when that automatic disabling happens. It is the one notification that reaches you rather than them, and it needs no Duckday account — which is the point, since you probably do not have one. The address is optional, can be changed or cleared at any time from the same settings page, and is written only on that event: never on a single failed delivery. One address receives at most 5 of these alerts per day (UTC), however many endpoints name it.

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
todos.created / todos.updated / todos.deleted To-Do

*.deleted fires on soft-deletion (the platform's standard delete). The catalog grows together with the resource catalog — see the changelog.