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).
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).
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: healthy → failing (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.