REST API: Webhooks
ScaiKey-signed webhook receiver. Updates the local user/group mirror in near-real-time on join/leave events.
POST /v1/webhooks/scaikey#
ScaiKey posts signed events here. The handler verifies the HMAC-SHA256 signature using SCAIFLOW_SCAIKEY_WEBHOOK_SECRET, then dispatches to per-event-type handlers.
Headers (set by ScaiKey, verified server-side):
X-Scaikey-Signature: t=<timestamp>,v1=<hex_sha256>— HMAC overtimestamp.bodywith the shared secret.X-Scaikey-Timestamp— used both in the signature payload and for replay-attack protection.Content-Type: application/json.
Body — a ScaiKey webhook envelope:
{
"type": "user.created", // event type
"id": "evt_xxx",
"created_at": "2026-04-29T...",
"data": { // type-dependent payload
"user_id": "usr_xxx",
"tenant_id": "tnt_acme",
"email": "alice@acme.example"
}
}
Response:
{ "handled": true } // or false for unknown event types
Unknown event types return 200 + handled: false (so ScaiKey doesn't retry them — they're just ignored).
Bad/missing signatures return 400; missing timestamp or wrong secret config return 400/503 respectively.
Supported event types#
| Type | Behavior |
|---|---|
user.created |
Upsert the user row in the local mirror. |
user.updated |
Upsert (email/name/status changes). |
user.deleted |
Soft-delete (preserves admin_role + audit trail). |
group.created |
Upsert the group row. |
group.updated |
Upsert. |
group.deleted |
Soft-delete. |
group_membership.added |
Upsert (user_id, group_id) into the memberships table. Triggers super_admin re-reconciliation. |
group_membership.removed |
Soft-delete the membership. Triggers super_admin re-reconciliation. |
application.group_assigned |
Refresh effective_users + re-reconcile super_admin. Does NOT create a Group row (mirroring is via the regular sync). |
application.group_unassigned |
Same as assigned. |
Anything else returns 200 + handled: false.
Fallback: scheduled sync#
If webhooks aren't reaching the backend (network issue, ScaiKey misconfiguration), the hourly background sync (interval set by SCAIFLOW_SYNC_INTERVAL_MINUTES, default 60) catches up. The CLI bootstrap (scaiflow scaikey-register) does the initial sync.
Configuration#
Two env vars on the backend:
SCAIFLOW_SCAIKEY_WEBHOOK_SECRET— the shared secret used for signature verification. Get it from your ScaiKey app's webhook configuration UI.- (no second env var) — the webhook URL is whatever you configure in ScaiKey's app settings. Typically
https://scaiflow.example/api/v1/webhooks/scaikey.
If SCAIFLOW_SCAIKEY_WEBHOOK_SECRET is unset, the endpoint returns 503 (fail-closed). Set it before pointing ScaiKey at the URL.
Testing locally#
ScaiKey doesn't reach localhost. For local development, either:
- Tunnel through ngrok/cloudflared and configure ScaiKey to point at the tunnel URL.
- Trigger sync manually with
POST /v1/admin/sync(uses the API path, not webhooks).