---
title: "Events reference \u2014 overview"
path: reference/events/overview
status: published
---

# 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](./catalog).

## Envelope

Every event ships in the same outer envelope:

```json
{
  "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](./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`:

```
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
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
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
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](./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](../api/admin-webhook-subscriptions). Each subscription carries a name, target URL, list of topic patterns (glob), and an inline secret or vault path.

## See also

- [Catalog](./catalog) — full topic list with payload shapes and sample payloads.
- [Webhooks](../webhooks) — operational page on the dispatcher itself.
- [Concepts: webhooks](../../concepts/webhooks) — conceptual overview.
