← Docs

Webhooks

Real signed HTTPS delivery to a URL your Organization registers — the external half of this platform's event-delivery lane (the internal half is real-time push to this dashboard itself, see Sessions). Requires Plan.webhooks (Growth and above); re-checked on every dispatch, not just at subscription creation — a downgraded Organization's existing subscriptions simply stop firing.

POST /api/v1/webhooks/subscriptions/
{
  "url": "https://your-app.example.com/webhooks/onehux",
  "event_types": ["login", "session_revoked"],
  "is_active": true
}

An empty event_types list means every event type. The response includes secret — shown exactly once, at creation (and again on POST .../{id}/rotate-secret/) — stored encrypted server-side (Fernet, not hashed: unlike a login credential, this secret must be read back in plaintext on every delivery to compute that delivery's signature).

Verifying a delivery

Every request carries two headers, computed fresh per attempt — a retried delivery gets a new timestamp/signature pair, never a replayed stale one:

X-OneHux-Signature: <hex HMAC-SHA256>
X-OneHux-Timestamp: <unix seconds>

Recompute it yourself as HMAC-SHA256(secret, "{timestamp}.{raw_body}") — sign the timestamp and the exact raw request body, concatenated with a single ., using your subscription's own secret — and compare against X-OneHux-Signature with a constant-time comparison. Reject anything that doesn't match, and reject a timestamp too far in the past (a reasonable window — a few minutes — protects against a captured request being replayed later).

Retry, health, and auto-disable

A failed delivery (connection error, timeout, or a non-2xx response) retries with exponential backoff — up to 5 attempts total by default, capped at a 600-second delay between attempts, both configurable per-deployment. Your endpoint must be safe to call more than once for the same event: retries are a normal, expected part of delivery, not an error condition on your side.

A subscription's own status tracks health from consecutive_failures: healthyfailing (5+ consecutive failures, still being retried) → disabled (15+ — no further attempts until you fix the endpoint and re-activate it, PATCH { "is_active": true }). The most recent failure reason is in last_error, so "failing"/"disabled" is never an unexplained stuck state.

Every attempt (not just the final outcome) is a real, durable row: GET /api/v1/webhooks/subscriptions/{id}/deliveries/ lists them — response status, response body (truncated to 2000 chars), duration, and attempt number — a real delivery log, not just something inferred from the current status.