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

Event catalog

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

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
1
2
3
4
5
6
7
8
9
{
  "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
1
2
3
4
5
6
7
8
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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
1
2
3
4
5
6
7
8
9
{
  "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
1
2
3
4
5
6
7
8
{
  "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
1
2
3
4
5
6
7
8
9
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "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
Updated 2026-05-18 01:48:41 View source (.md) rev 2