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

Events reference — overview

ScaiControl emits a defined set of webhook events for every billing- and subscription-related state change. This page covers the envelope, signing, and delivery semantics. The full topic catalog with payload schemas is in Catalog.

Envelope#

Every event ships in the same outer envelope:

json
1
2
3
4
5
6
7
8
9
{
  "event_id": "<uuid-v4>",
  "event_type": "subscription.activated",
  "event_version": "1.0",
  "occurred_at": "2026-05-10T14:32:11+00:00",
  "source": "scaicontrol",
  "idempotency_key": "subscription:sub_abc:activated:initial",
  "data": { /* per-topic */ }
}
Field Purpose
event_id UUIDv4 unique to this delivery. Also echoed in X-ScaiControl-Event-Id header.
event_type Unprefixed topic name (e.g. subscription.activated). The scaicontrol. prefix and .vN suffix are implicit in source + event_version.
event_version Semver-ish MAJOR.MINOR. Breaking field changes bump MAJOR.
occurred_at RFC 3339 UTC of when the underlying domain change happened — NOT when this delivery was attempted.
source Always "scaicontrol".
idempotency_key Stable across retries. Format: <resource_type>:<resource_id>:<event_type>:<lifecycle_step>. Use it for inbox dedup.
data Per-topic payload. See Catalog for shapes.

The full envelope JSON Schema is at https://www.scailabs.ai/docs/scaicontrol/reference/events/envelope-schema.

Headers#

POSTed to the subscriber's target_url:

carbon
1
2
3
4
5
6
Content-Type: application/json
X-ScaiControl-Signature: sha256=<hex>
X-ScaiControl-Timestamp: <unix-seconds>
X-ScaiControl-Event-Id: <uuid>
X-ScaiControl-Event-Type: <event_type>
User-Agent: ScaiControl-Webhook/1.0

Signature verification#

python
1
2
3
4
5
import hmac, hashlib

def verify(secret: bytes, raw_body: bytes, sig_header: str) -> bool:
    expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig_header)

The secret is per-subscriber, configured at subscription registration. Use HMAC-SHA256 over the raw request body (not a parsed JSON re-serialisation — whitespace would differ).

Replay protection#

python
1
2
3
4
5
6
import time
def fresh(timestamp_header: str, window_seconds: int = 300) -> bool:
    try:
        return abs(time.time() - int(timestamp_header)) <= window_seconds
    except ValueError:
        return False

A subscriber that gets a delivery whose X-ScaiControl-Timestamp is more than 5 minutes off should return 401. ScaiControl will retry; if it's still too old by the next attempt, that's a system-clock issue worth alerting on.

Delivery semantics#

At-least-once. Subscribers must dedup via event_id or idempotency_key (or both).

Retry schedule for 5xx / timeout / network errors:

Attempt # Delay before next retry
1 (first) (initial — no wait)
2 1 minute
3 5 minutes
4 30 minutes
5 2 hours
6 12 hours
7 24 hours
After 7 attempts Marked dead, no more retries

Response code semantics (from the subscriber's reply):

Code Interpretation
200 OK (or any 2xx) Accepted. Delivery row → dispatched.
409 Conflict Idempotent ack (subscriber already saw it). Treated as success.
400 Bad Request Malformed. Don't retry. Row → dead. Alerting recommended on the subscriber side.
401 Unauthorized Signature or replay failure. Don't retry — this is config drift, not transient. Row → dead.
Other 4xx Don't retry. Row → dead.
503 Service Unavailable or other 5xx Retry with exponential backoff.
Timeout / network error Same as 5xx.

Subscriber inbox pattern#

The reference shape for the subscriber's inbox:

sql
1
2
3
4
5
6
7
8
9
CREATE TABLE webhook_inbox (
  id            BIGINT PRIMARY KEY,
  event_id      UUID NOT NULL UNIQUE,        -- dedup key
  event_type    TEXT NOT NULL,
  received_at   TIMESTAMP NOT NULL DEFAULT now(),
  processed_at  TIMESTAMP,
  payload       JSONB NOT NULL,
  status        TEXT NOT NULL DEFAULT 'pending'
);

Subscribe path:

  1. Receive POST.
  2. Verify signature + timestamp.
  3. INSERT … ON CONFLICT DO NOTHING on (event_id). If skipped, return 200 (or 409).
  4. Return 200 immediately. Process the row asynchronously.

This pattern means slow processing never blocks ScaiControl's dispatcher — the 200 ack lets the next event in.

Versioning policy#

  • Patch / minor field additions to a topic don't bump the version. Subscribers should ignore unknown fields.
  • Breaking changes (field removal, type narrowing, semantic shift) ship the new shape under a new major (v2). For at least one full quarter, both v1 and v2 are emitted concurrently — subscribers migrate at their own pace, then we stop emitting v1.
  • Topic removal never happens silently. Deprecation notice → 3-month grace period → silent producer removal. Schemas stay published indefinitely so old fixtures still validate.

What's NOT in the launch catalog#

Topics deferred past MVP (see Catalog for the comparison with what consumers like ScaiCRM expect):

  • Invoice events (invoice.issued, invoice.paid, invoice.overdue).
  • Dunning escalation.
  • Usage aggregates (daily, monthly).
  • Catalog product events.
  • Provisioning rejection.
  • subscription.renewed.v1 — folded into subscription.changed.v1 with change_kind="renewal".

These topics will arrive as the corresponding features ship.

Subscriber management#

Manage subscribers via the admin UI at /admin/webhook-subscriptions or via the API at /api/v1/admin/webhook-subscriptions — see Admin — webhook subscribers. Each subscription carries a name, target URL, list of topic patterns (glob), and an inline secret or vault path.

See also#

  • Catalog — full topic list with payload shapes and sample payloads.
  • Webhooks — operational page on the dispatcher itself.
  • Concepts: webhooks — conceptual overview.
Updated 2026-05-18 01:48:41 View source (.md) rev 2