---
title: Event catalog
path: reference/events/catalog
status: published
---

# Event catalog

The full topic catalog in the launch (v1.0) emission. See [Overview](./overview) for the envelope and signing.

## Billing-link events

### `tenant.billing_linked.v1`

Emitted when a tenant first gains a `tenant_billing_profiles` row. Signals "this tenant is now a billable entity"; subscribers can create a corresponding CRM record.

```json
{
  "tenant_id":             "tnt_…",
  "partner_id":            "prt_…",
  "scaicontrol_customer_id": "tnt_…",
  "linked_at":             "2026-05-10T09:00:00+00:00",
  "company_name":          "Servantus B.V.",
  "country_code":          "NL",
  "vat_number":            "NL867406598B01"
}
```

### `partner.billing_linked.v1`

Emitted when a partner first gains `legal_name` AND `seller_country_code` set.

```json
{
  "partner_id":            "prt_…",
  "scaicontrol_customer_id": "prt_…",
  "linked_at":             "2026-05-10T09:00:01+00:00",
  "legal_name":            "Servantus B.V.",
  "country_code":          "NL",
  "vat_number":            "NL867406598B01"
}
```

### `tenant.billing_updated.v1` / `partner.billing_updated.v1`

Emitted on subsequent edits to billing-relevant fields. Carries `changed_fields[]` so subscribers can do selective re-sync.

```json
{
  "tenant_id":             "tnt_…",
  "partner_id":            "prt_…",
  "scaicontrol_customer_id": "tnt_…",
  "updated_at":            "2026-05-12T15:22:00+00:00",
  "changed_fields":        ["contact_email", "phone"],
  "company_name":          "Servantus B.V.",
  "country_code":          "NL",
  "vat_number":            "NL867406598B01",
  "contact_email":         "billing@servantus.nl"
}
```

## Subscription lifecycle events

### `subscription.activated.v1`

A single-service subscription becomes active or trialing. Pack subscriptions use `pack_subscription.activated.v1` instead.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "tenant_id":             "tnt_…",
  "partner_id":            "prt_…",
  "state":                 "active",
  "service_slug":          "scaikey",
  "service_name":          "ScaiKey",
  "plan_key":              "scaikey.starter",
  "plan_id":               "<uuid>",
  "plan_name":             "ScaiKey Starter",
  "quantity":              1,
  "current_period_start":  "2026-05-10T09:01:00+00:00",
  "current_period_end":    "2026-06-10T09:01:00+00:00",
  "trial_end_date":        null,
  "next_billing_date":     "2026-06-10T09:01:00+00:00",
  "mrr_amount_cents":      1900,
  "currency":              "EUR",
  "activated_at":          "2026-05-10T09:01:00+00:00"
}
```

### `subscription.changed.v1`

Plan change, status transition, scheduled cancellation, renewal. The `change_kind` discriminator tells subscribers what kind of change.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "state":                 "active",
  "service_slug":          "scaikey",
  "plan_key":              "scaikey.pro",
  "plan_id":               "<uuid>",
  "plan_name":             "ScaiKey Professional",
  "current_period_start":  "2026-05-10T09:01:00+00:00",
  "current_period_end":    "2026-06-10T09:01:00+00:00",
  "mrr_amount_cents":      9900,
  "currency":              "EUR",
  "change_kind":           "plan_change",
  "previous":              { "plan_key": "scaikey.starter", "plan_id": "<old-uuid>", "mrr_amount_cents": 1900 },
  "pending_cancellation_at": null,
  "changed_at":            "2026-05-20T14:00:00+00:00"
}
```

`change_kind` values: `plan_change`, `quantity_change`, `renewal`, `scheduled_cancellation`, `scheduled_cancellation_undone`, `status_change`.

When `change_kind="scheduled_cancellation"`, the `state` is `cancelling` and `pending_cancellation_at` is set to the future cancel date.

### `subscription.cancelled.v1`

The subscription reached terminal `cancelled`. Either from an immediate admin cancel, or from the reaper flipping `cancelling → cancelled` at period end. The `effective_immediately` flag tells subscribers which:

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "service_slug":          "scaikey",
  "cancelled_at":          "2026-06-01T11:15:00+00:00",
  "cancellation_reason":   "Customer downgraded to ScaiVault only",
  "effective_immediately": true,
  "terminal_state":        "cancelled"
}
```

`terminal_state` is `cancelled` for admin/customer cancellation and `expired` for time-bound subscriptions that ran their term — both treated the same by most subscribers.

### `subscription.suspended.v1`

Admin-driven pause, or auto-suspend after sustained `past_due`. Subscribers map this to a paused state.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "service_slug":          "scaikey",
  "suspended_at":          "2026-05-22T10:00:00+00:00",
  "reason":                "admin_pause",
  "previous_state":        "active"
}
```

### `subscription.resumed.v1`

Suspended → active.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "service_slug":          "scaikey",
  "resumed_at":            "2026-05-25T09:00:00+00:00",
  "state":                 "active"
}
```

### `subscription.trial_ending.v1`

Fired by the daily trial monitor cron at `days_remaining ∈ {7, 3, 1}`. Once per (subscription, threshold) — dedup state lives in `subscription.metadata_["trial_ending_fired"]`.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "service_slug":          "scaikey",
  "plan_key":              "scaikey.trial",
  "trial_end_date":        "2026-05-24T00:00:00+00:00",
  "days_remaining":        3
}
```

### `subscription.payment_failed.v1`

A scheduled payment failed. Subscribers typically mirror as `payment_status='past_due'` on their copy of the subscription.

```json
{
  "subscription_id":       "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "service_slug":          "scaikey",
  "invoice_number":        "SCAI-2026-000042",
  "attempt_at":            "2026-06-10T09:05:00+00:00",
  "amount_cents":          9900,
  "currency":              "EUR",
  "attempt_number":        1,
  "failure_code":          "card_declined",
  "failure_reason":        "Your card was declined.",
  "next_retry_at":         "2026-06-13T09:05:00+00:00",
  "payment_provider":      "stripe"
}
```

## Pack subscription lifecycle

### `pack_subscription.activated.v1`

A pack subscription was created. The `pack_includes` array enumerates the constituent services — child subscriptions are NOT emitted independently.

```json
{
  "pack_subscription_id":  "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "state":                 "trialing",
  "pack_slug":             "trial-bundle",
  "pack_name":             "ScaiLabs Trial Bundle",
  "pack_includes": [
    { "service_slug": "scaikey",   "plan_key": "scaikey.trial",   "plan_id": "<uuid>", "override_price_cents": null },
    { "service_slug": "scaivault", "plan_key": "scaivault.trial", "plan_id": "<uuid>", "override_price_cents": null }
  ],
  "current_period_start":  "2026-05-10T09:00:30+00:00",
  "current_period_end":    "2026-05-24T09:00:30+00:00",
  "trial_end_date":        "2026-05-24T09:00:30+00:00",
  "mrr_amount_cents":      0,
  "currency":              "EUR",
  "activated_at":          "2026-05-10T09:00:30+00:00"
}
```

### `pack_subscription.changed.v1`

(Reserved.) Pack mutations aren't currently exposed via an endpoint — no producer wired up yet. The topic + schema exist for when one lands.

### `pack_subscription.cancelled.v1`

Pack cancellation. Child subscriptions are torn down implicitly.

```json
{
  "pack_subscription_id":  "<uuid>",
  "owner_kind":            "tenant",
  "scaicontrol_customer_id": "tnt_…",
  "pack_slug":             "trial-bundle",
  "pack_name":             "ScaiLabs Trial Bundle",
  "cancelled_at":          "2026-07-01T00:00:00+00:00",
  "cancellation_reason":   "End of trial — customer chose individual services",
  "effective_immediately": true
}
```

## ID conventions across events

| Field | Format | Notes |
|---|---|---|
| `event_id` | UUIDv4 | Per-delivery unique |
| `tenant_id`, `partner_id` | `tnt_<alphanum>` / `prt_<alphanum>` | Synced from ScaiKey |
| `subscription_id`, `pack_subscription_id`, `plan_id` | UUIDv4 | DB-internal IDs |
| `scaicontrol_customer_id` | Equals `tenant_id` (for `owner_kind="tenant"`) or `partner_id` (for `owner_kind="partner"`) | Pass-through, no separate space |
| `plan_key` | `<service_slug>.<plan_slug>` | Stable across re-seeds; pair with `plan_id` |
| `service_slug` | `[a-z0-9_]+` | E.g. `scaikey`, `scaivault` |
| `pack_slug` | `[a-z0-9_-]+` | E.g. `trial-bundle` |
| `currency` | ISO 4217 | EUR, USD, GBP |
| Timestamps | RFC 3339 UTC | Always `+00:00` suffix |

## State mapping

ScaiControl ships eight subscription states; most subscribers normalise to fewer:

| ScaiControl | Typical CRM-side state |
|---|---|
| `pending` | (not surfaced — sub-second transient) |
| `trialing` | `trial` |
| `active` | `active` |
| `past_due` | `past_due` |
| `cancelling` | `active` + `pending_cancellation_at` field |
| `cancelled` | `cancelled` |
| `expired` | `cancelled` |
| `suspended` | `paused` |
