Event catalog
Every async state change in heliumOS emits a webhook event. This
catalog enumerates the event types you can subscribe to; payload
shapes are documented in the API reference under the
Events tag. The authoritative, always-current list is served at
GET /v1/reference/event_types, and webhook endpoint
enabled_events is validated against it.
Shape
Every event carries:
{
"id": "evt_...",
"type": "sim.activated",
"created_at": "2026-04-24T15:00:00Z",
"livemode": false,
"data": { "object": { ... } },
"idempotency_key": "..."
}
Subscriber events
| Type | Fires when |
|---|---|
subscriber.created | A subscriber record is created, whether through POST /v1/subscribers or the migration import (import-originated events carry data.imported: true) |
subscriber.locked | A subscriber's SIM-swap / port-out protection lock is turned on (locked_status becomes user_initiated_lock) |
subscriber.unlocked | A subscriber's SIM-swap / port-out protection lock is turned off (locked_status becomes unlocked) |
Subscriber event payloads are always PII-redacted: email, phone, and
date_of_birth are null and the object carries pii_redacted: true.
Read the subscriber back through GET /v1/subscribers/{id} (subject to
your role's PII permissions) when you need the contact fields.
Subscription events
| Type | Fires when |
|---|---|
subscription.created | A subscription record is created, whether through POST /v1/subscriptions (solo or group) or the migration import (import-originated events carry data.imported: true) |
subscription.updated | A status or pending-plan-change reconciliation with no dedicated event below, such as a scheduled downgrade being set or cleared |
subscription.plan_switched | A plan change applies (an upgrade immediately, a scheduled downgrade at renewal) |
subscription.paused | Billing is put on hold — moves to paused, and a void collection pause appears on the object. Service is not interrupted; the SIMs keep working |
subscription.resumed | Billing restarts — moves from paused back to active and the collection pause is cleared |
subscription.cancelled | Moves to cancelled |
subscription.terminated | Moves to terminated |
subscription.past_due | Payment is overdue and the subscription moves to past_due |
subscription.unpaid | Payment retries are exhausted and collection has stopped |
subscription.recovered | A past_due subscription recovers to active after a successful payment |
subscription.addon_added | An add-on is attached to the subscription |
subscription.addon_removed | An add-on is removed from the subscription |
subscription.addon_expired | A time-bounded add-on reaches the end of its term |
SIM events
The eSIM install code rides
sim.provisioned,sim.activatedandsim.swapped— but only for an endpoint that subscribes to them on purpose. Theactivation_code(LPA) string installs the subscriber's profile onto whatever device scans it, so it is delivered only whereenabled_eventsnames the type directly (sim.*, or the exact type). An endpoint left on the default catch-all*still receives every one of these events; theactivation_codefield is simply absent from the body.Building an eSIM flow? Subscribe with
sim.*rather than*. Wiring up an analytics sink or a general-purpose connector?*is the right choice, and it keeps install credentials out of it.
| Type | Fires when |
|---|---|
sim.provisioned | A SIM record is created and provisioned |
sim.imported | An existing SIM is imported (migration) |
sim.activation_started | Activation is requested from the carrier |
sim.activated | Carrier confirms activation |
sim.activation_failed | Carrier rejects activation |
sim.activation_resolution_required | Activation stalled and needs operator resolution |
sim.suspended | Moves to suspended |
sim.restored | Moves from suspended back to active |
sim.deactivated | Terminal deactivation |
sim.swapped | A new SIM takes over the MSISDN |
sim.reassigned | The SIM's placement moves — a different subscription, owner, or holder line (an explicit reassign, or a leftover SIM reused by a re-subscribe). The object carries the new placement (subscriber_id = the payer, holder_subscriber_id = the member who uses the line, subscription_id, subscription_line_id, external_subscriber_id); previous_attributes carries the old subscription_id, subscriber_id, subscription_line_id, and subscription_status. Mirroring the end user? Read holder_subscriber_id, not subscriber_id — the latter is the payer here but the link target on sim.linked. Re-pointing a mirrored subscription? Only when previous_attributes.subscription_status is cancelled. |
sim.products_updated | The SIM's provisioned products (SLOs) change |
sim.msisdn_changed | The SIM's MSISDN changes |
sim.boosted | A data boost is applied |
sim.boost_expired | A data boost reaches the end of its window |
sim.throttled | The SIM is throttled after crossing a usage cap |
sim.throttle_cleared | Throttling is lifted (cycle rollover or manual clear) |
sim.port_out_pin_generated | A port-out PIN is generated for the SIM |
sim.port_out_pin_cleared | The SIM's port-out PIN is cleared |
Port events
| Type | Fires when |
|---|---|
port.created | A port-in request record is created |
port.submitted | The port-in request is accepted by heliumOS |
port.in_progress | The carrier has the port-in in flight |
port.resolution_required | The port-in stalled and needs operator or subscriber input. status_reason carries the carrier's own message (e.g. 6B-T-Mobile Number Transfer PIN is required or incorrect) and status_reason_code its message code (6B) — branch on the code, show the text |
port.resolution_expiring | A stalled port-in is nearing the carrier's deadline (~30 days from submission) after which the transfer is cancelled and the line switched off. Fires once per submission, around day 21. Carries the same status_reason / status_reason_code, plus submitted_at and days_since_submitted |
port.completed | The number transfer completed |
port.failed | The port-in failed |
port.cancelled | The port-in was cancelled |
port.out_validation_requested | The gaining carrier requested port-out validation |
port.out_completed | A port-out finalized and the line left heliumOS |
Usage events
| Type | Fires when |
|---|---|
usage.data_threshold_crossed | A configured data-usage threshold is crossed |
usage.voice_threshold_crossed | A configured voice-usage threshold is crossed |
usage.sms_threshold_crossed | A configured SMS-usage threshold is crossed |
usage.tier_changed | The SIM moves between throttle tiers |
usage.cycle_rolled_over | A billing cycle rolls over and counters reset |
Billing events
| Type | Fires when |
|---|---|
invoice.upcoming | A pre-renewal notice ahead of the next billing cycle (a preview invoice object, status: "upcoming", no id yet). Sandbox environments emit it a few days before current_period_end; environments on a live Stripe connection relay Stripe's own invoice.upcoming, which must be enabled on the Stripe webhook endpoint |
invoice.created | An invoice is created |
invoice.finalized | An invoice is finalized and ready to collect |
invoice.paid | An invoice is paid |
invoice.payment_failed | A payment attempt on the invoice fails |
invoice.voided | An invoice is voided |
payment.succeeded | A payment succeeds |
payment.failed | A payment fails |
payment.refunded | A payment is refunded |
payment_method.attached | A payment method is attached to a subscriber |
payment_method.updated | A payment method becomes the subscriber's default |
payment_method.detached | A payment method is detached from a subscriber |
pSIM order events
| Type | Fires when |
|---|---|
psim_order.paid | A paid pSIM order is placed (the card is charged) |
psim_order.created | A free pSIM order is placed (no charge) |
psim_order.refunded | A pSIM order's charge is refunded (or a paid order is canceled) |
psim_order.canceled | A free pSIM order is canceled |
The recipient email and shipping_address are redacted in the event store
and injected into the outbound webhook body at delivery time, so your
fulfilment backend receives a complete order.
CPNI step-up events
When a support agent needs to view a subscriber's PII or change their account, heliumOS issues a one-time verification code and asks you to deliver it to the subscriber over your own channel (SMS, app push, email). The subscriber reads it back to the agent, who enters it to unlock access for a window.
| Type | Fires when |
|---|---|
cpni.verification.requested | An agent started a step-up challenge, carries the one-time code |
cpni.verification.succeeded | The agent entered the correct code |
cpni.verification.failed | The challenge locked out after too many attempts |
cpni.verification.requested is the only event whose payload
contains a secret: data.object.code. Deliver it to the subscriber and
treat it like a password: don't log it, and don't store it. The code is
short-lived (operator-configured, default 10 minutes) and is never
returned by the GET /events API; it rides the signed webhook only.
{
"type": "cpni.verification.requested",
"data": {
"object": {
"object": "cpni_verification",
"id": "cpniv_01HGZX...",
"subscriber_id": "sbr_01HGZX...",
"agent": { "user_id": "...", "email": "agent@operator.example" },
"code": "482913",
"expires_at": "2026-06-08T17:10:00Z",
"grant_duration_seconds": 1800
}
}
}
Carrier connection events
| Type | Fires when |
|---|---|
carrier_connection.verified | A scheduled verify call succeeded |
carrier_connection.auth_failed | A verify call failed authentication |
carrier_connection.credentials_expired | The connection's carrier credentials expired |
Event types are additive: new types may appear without a major
version bump. Handlers should ignore unknown type values.