Platform
ScaiWave ScaiGrid ScaiCore ScaiBot ScaiDrive ScaiKey Models Tools & Services
Solutions
Organisations Developers Internet Service Providers Managed Service Providers AI-in-a-Box
Resources
Support Documentation Blog Downloads
Company
About Research Careers Investment Opportunities Contact
Log in

Webhooks

ScaiKey emits webhooks for partner, tenant, user, group, application, session, registration, and authentication events. Webhooks are delivered with retry, signed with HMAC-SHA256, and queued through a background worker.

Envelope#

Every payload follows the same shape, regardless of event type:

json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "event_id":   "evt_<nanoid>",
  "event_type": "<event-name>",
  "timestamp":  "<iso 8601 UTC>",
  "resource":   { "type": "<resource-type>", "id": "<resource-id>" },
  "actor":      { "id": "<user-id|null>", "type": "admin|user|scim|system|api" },
  "data":       { /* per-event payload, see below */ },
  "tenant_id":  "tnt_<id>",
  "partner_id": "prt_<id>"
}

tenant_id is present for tenant-scoped events; partner_id for partner-level events. Both can be present (tenant events carry the owning partner_id too).

Delivery headers#

Each delivery includes:

Header Example Meaning
Content-Type application/json
X-ScaiKey-Webhook-ID 4291 Numeric delivery row id (for replay correlation)
X-ScaiKey-Event-ID evt_a3f9k2bWqL8Hn5pZ The same event_id from the body
X-ScaiKey-Event-Type tenant.created The same event_type from the body
X-ScaiKey-Timestamp 1747584000 Unix seconds; matches the signature timestamp
X-ScaiKey-Signature t=1747584000,v1=<hex> HMAC-SHA256 — see below
User-Agent ScaiKey-Webhook/1.0

Signature verification#

The signature is HMAC-SHA256(secret, "{timestamp}.{body}") where {body} is the raw request body bytes. The body is canonically formatted as json.dumps(payload, separators=(",", ":"), sort_keys=True) — your verifier must compare against the bytes actually received, not a re-serialized version.

python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import hmac, hashlib, time

def verify(headers, body_bytes, secret):
    sig_header = headers.get("X-ScaiKey-Signature", "")
    parts = dict(p.split("=", 1) for p in sig_header.split(","))
    ts = int(parts["t"])
    received = parts["v1"]

    # Reject if timestamp is too old (replay window)
    if abs(time.time() - ts) > 300:
        return False

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{ts}.{body_bytes.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(received, expected)

Event types#

The full enum is in backend/src/scaikey/services/events.py. Headline list:

Partner / tenant lifecycle:

  • partner.created, partner.updated, partner.deleted
  • tenant.created, tenant.updated, tenant.deleted

There's no partner.suspended or tenant.suspended — suspending one of those emits the .updated event with data.status = "SUSPENDED".

User lifecycle:

  • user.created, user.updated, user.deleted
  • user.suspended, user.activated (dedicated events, unlike tenants)
  • user.password_changed, user.mfa_enabled, user.mfa_disabled

Group lifecycle:

  • group.created, group.updated, group.deleted
  • group.member_added, group.member_removed — one event per user added/removed in batch operations
  • group.nested_added, group.nested_removed — for group-of-groups

Application lifecycle:

  • application.created, application.updated, application.deleted
  • application.user_assigned, application.user_unassigned
  • application.group_assigned, application.group_unassigned

Session and auth events:

  • session.created, session.terminated
  • auth.login_success, auth.login_failed, auth.logout

Registration requests:

  • registration_request.created, registration_request.approved, registration_request.rejected

Sample payloads#

Synthetic but accurate payload examples for every event family are committed in the open-source backend repo at docs/integration/sample-webhooks/. Use them as fixture data for your translator's tests — they reflect the exact data shape per event.

Per-event data field shape#

A compact reference for the fields inside data. Tenant_id and partner_id sit in the envelope, not in data.

Event data keys
partner.created id, name, slug, status
partner.updated echo of PATCH body (any subset of name, slug, status)
partner.deleted id, name, slug
tenant.created id, name, slug, partner_id, status
tenant.updated echo of PATCH body, minus settings and branding (filtered for size)
tenant.deleted id, name, slug, partner_id
user.created email, display_name, first_name, last_name, status
user.updated echo of PATCH body, minus password
user.deleted email, display_name
group.created name, description, group_type
group.updated echo of PATCH body
group.deleted name
group.member_added / group.member_removed group_name, user_id
group.nested_added / group.nested_removed parent_group_id, parent_group_name, child_group_id, child_group_name

Routing model#

ScaiKey supports two webhook scopes:

  • TENANT-scoped webhooks — register a URL on a tenant; receive every event in that tenant.
  • APPLICATION-scoped webhooks — register a URL on a tenant plus an application_id; receive only events that involve users or groups assigned to that application.

A subscription's events list accepts exact event names (user.created), wildcards (user.*), or universal (*).

Per-app sync webhooks#

Separate from the subscription model: an application can configure a sync_webhook_url + sync_webhook_secret on its row to receive events about its assigned users/groups without registering an explicit subscription. Useful for downstream apps that just want directory mirroring without going through the full webhook UI.

Sync webhook secrets are stored as plaintext (signing uses the raw value, unlike subscription webhooks where the secret is hashed).

Reliability#

Failures are retried automatically with backoff at 1, 5, and 15 minutes (3 attempts total). After permanent failure, the delivery is marked failed and the webhook's consecutive_failures counter increments. The admin UI surfaces delivery stats per webhook (success_rate, avg_response_time_ms).

Your endpoint should:

  • Return 2xx within 10 seconds (timeout, configurable up to 30).
  • Be idempotent — duplicate deliveries are possible during retry.
  • Verify the signature before doing any work (defense against forged calls).
Updated 2026-05-17 12:20:38 View source (.md) rev 1