Accountflow Developer next

Webhooks

Signed thin events: the poke, then you fetch.

Bridge notifies your systems about changes with thin, signed events: the payload tells you what happened to which resource — you fetch current state through the API. This "poke, then fetch" model is deliberate: payloads never carry entity bodies, so a leaked delivery leaks references, not books, and you always render from fresh state.

Delivery contract — read this before building

Event payload

{
  "id": "9a3f6d0e-8c2b-4d51-b1a7-2f4e5d6c7b8a",
  "type": "company.created",
  "occurred_at": "2026-08-20T12:34:56.789Z",
  "data": {
    "resource_type": "company",
    "resource_id": "b1c2d3e4-…",
    "organization_id": "a0b1c2d3-…"
  }
}

Event types mirror the audited actions, e.g. company.created, company.updated, company.disabled, company.deleted, company.restored, access_policy.created, access_policy.assigned, access_policy.revoked, webhook_endpoint.created, job.succeeded, job.failed. Subscribe to specific types at registration, or to everything with an empty filter (note: an all-events endpoint also receives its own webhook_endpoint.* lifecycle events).

Registration handshake

A new endpoint starts in pending_verification. Bridge POSTs a webhook.verification event carrying a challenge:

{ "type": "webhook.verification", "data": { "challenge": "f1ac40a6…" } }

Respond 2xx with the challenge echoed anywhere in the response body (e.g. {"challenge": "f1ac40a6…"}). The endpoint then turns active and events flow. Changing the endpoint's URL re-runs the handshake.

Signature verification

Every delivery carries:

Webhook-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>

where v1 = HMAC-SHA256(secret, "<t>" + "." + <raw request body>), hex-encoded. The secret (whsec_…) is returned exactly once when you create the endpoint or rotate the secret.

Verify like this:

  1. Parse t and v1 from the header.
  2. Reject if |now − t| exceeds your tolerance (5 minutes is a good default) — this defeats replay of captured deliveries.
  3. Compute HMAC-SHA256(secret, t + "." + body) over the raw body bytes (before any JSON parsing) and compare to v1 with a constant-time comparison.

Test vector

field value
secret whsec_testsecret
t 1700000000
body {"id":"evt_00000000-0000-4000-8000-000000000001","type":"company.created"}
signature t=1700000000,v1=c9be8761917e51cda36abc9ae6eaa0d6cb5c4a0874f22dd6efb6c3655b6ff018

(Asserted by WebhookSignerTest — the vector is a wire contract.)

Pseudocode:

import hmac, hashlib, time

def verify(header: str, body: bytes, secret: str, tolerance_s: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(parts["t"]), parts["v1"]
    if abs(time.time() - t) > tolerance_s:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Endpoint requirements