docs

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

TypeFires when
subscriber.createdA subscriber record is created, whether through POST /v1/subscribers or the migration import (import-originated events carry data.imported: true)
subscriber.lockedA subscriber's SIM-swap / port-out protection lock is turned on (locked_status becomes user_initiated_lock)
subscriber.unlockedA subscriber's SIM-swap / port-out protection lock is turned off (locked_status becomes unlocked)
subscriber.updatedAny field on the subscriber record changes. previous_attributes carries the prior value of each changed field (contact fields redacted, as below). A change with nothing new in it emits nothing. A lock or payment-block change also emits its own event, so you never have to diff for one
subscriber.deletedA subscriber with no billing history is deleted through DELETE /v1/subscribers/{id}. Carries the full subscriber snapshot, since once the row is gone the event is the only record of it
subscriber.redactedA right-to-erasure request was carried out: the person's personal data was scrubbed. Distinct from subscriber.deleted — the record and its invoices and payment history stay, only the personal data is gone. data names the scopes and the fields cleared
subscriber.payment_updates_blockedThe subscriber is barred from adding a new card for a period, after a card decline that looked fraudulent. The object carries payment_updates_blocked_until and payment_updates_blocked_reason. Fires again if the block is renewed with a fresh window
subscriber.payment_updates_unblockedThat block is lifted early, before its window runs out
address.updatedA subscriber's billing, shipping or e911 address is created or changed. An e911 change has already been accepted by the carrier by the time this fires

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

TypeFires when
subscription.createdA subscription record is created, whether through POST /v1/subscriptions (solo or group) or the migration import (import-originated events carry data.imported: true)
subscription.updatedA status or pending-plan-change reconciliation with no dedicated event below, such as a scheduled downgrade being set or cleared
subscription.plan_switchedA plan change applies (an upgrade immediately, a scheduled downgrade at renewal)
subscription.line_addedA member joins a group subscription. Fires alongside subscription.updated, one per line, and names the line and the member on it
subscription.line_removedA member leaves a group subscription. Fires alongside subscription.updated, one per line. reason says why: requested (removed through the API or dashboard), scheduled (a departure booked for the end of the paid term landed), port_out (the member ported their number to another carrier), or null when the cause was not recorded
subscription.line.removal_scheduledA member's departure is booked for the end of a term the payer already paid for. Carries departs_at; the departure itself arrives later as subscription.line_removed
subscription.line.removal_canceledThe payer takes back a booked departure, and the member stays
subscription.scheduled_change_failedA booked change — a member's own downgrade, or one line of a whole-group billing-period switch — could not be applied at renewal and never will be. data.scheduled_change.reason says why. Without it your app would keep showing the pending change after it quietly stopped being pending
subscription.pausedBilling 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.resumedBilling restarts — moves from paused back to active and the collection pause is cleared
subscription.cancelledMoves to cancelled
subscription.terminatedMoves to terminated
subscription.past_duePayment is overdue and the subscription moves to past_due
subscription.unpaidPayment retries are exhausted and collection has stopped
subscription.recoveredA past_due subscription recovers to active after a successful payment
subscription.addon_addedAn add-on is attached to the subscription
subscription.addon_removedAn add-on is removed from the subscription
subscription.addon_expiredA time-bounded add-on reaches the end of its term
subscription.renewal_upcomingA prepaid multi-month term (3, 6 or 12 months) is about to renew for another term in one charge. Carries a subscription_renewal object — the net amount, the renewal date, the card it will land on, and one entry per seat on a group — so you can warn the payer and offer them a switch to monthly. The lead time is yours to set (renewal_reminder_lead_days, renewal_reminder_lead_days_annual on the operator). Monthly subscriptions never fire it; their pre-renewal notice is invoice.upcoming

SIM events

The eSIM install code rides sim.provisioned, sim.activated and sim.swapped — but only for an endpoint that subscribes to them on purpose. The activation_code (LPA) string installs the subscriber's profile onto whatever device scans it, so it is delivered only where enabled_events names the type directly (sim.*, or the exact type). An endpoint left on the default catch-all * still receives every one of these events; the activation_code field 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.

TypeFires when
sim.provisionedA SIM record is created and provisioned
sim.importedAn existing SIM is imported (migration)
sim.activation_startedActivation is requested from the carrier
sim.activatedCarrier confirms activation
sim.activation_failedCarrier rejects activation
sim.activation_resolution_requiredActivation stalled and needs operator resolution
sim.suspendedMoves to suspended
sim.restoredMoves from suspended back to active
sim.deactivatedTerminal deactivation
sim.clearedA SIM is released back to unassigned stock — an operator clear, or a card freed after a port-in was abandoned. data.state says what kind of stock it became: deactivated for a line that was torn down, provisioned for a never-activated card returned to the pool. The object carries the SIM's metadata; when a departing member's line is being held so they can still move their number, metadata.carrier_held_until is when that hold ends. previous_attributes carries what the card was released from: subscriber_id, subscription_id, subscription_line_id (the line it served, null if it sat on none) and msisdn
sim.linkedUnassigned stock is bound to a subscriber, on its own or as the first half of a number move. Carries the link target: on a group line that is the member, not the payer
sim.syncedA fresh read from the carrier is recorded on the SIM. Fires on every sync, including ones that change nothing, alongside any sim.suspended or sim.deactivated the sync found
sim.swappedA new SIM takes over the MSISDN
sim.reassignedThe 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, and subscription_status — the target subscription's own status when the SIM moved); 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, and take the new status from subscription_status rather than assuming active — the target can be past_due, and a pending_activation target that the move activates announces that on its own subscription event.
sim.products_updatedThe SIM's provisioned products (SLOs) change
sim.scam_block_changedThe line's scam blocking is switched on or off. Carries scam_block_enabled. Only available on plans that include scam protection
sim.video_quality_changedThe line's video streaming quality changes because its plan sets video_step_down_after_mb: lowered to 480p when the line's data use this cycle reaches it, raised back at the next cycle or when a data boost lifts it. Carries video_quality, previous_video_quality and video_stepped_down
sim.wps_changedThe line's Wireless Priority Service code is changed. Carries the new wps
sim.msisdn_changedThe SIM's MSISDN changes
sim.boostedA data boost is applied
sim.boost_expiredA data boost reaches the end of its window
sim.esim_profile_downloadedAn eSIM install profile is issued for the SIM. Carries the SIM and profile_expires_at, never the profile itself. Repeated downloads for one line are worth watching: that is what a hijacked-device install looks like
sim.boost_reducedPart of an accumulated data boost is taken back — a refunded or detached purchase reversed on a line still holding others. Carries the new boost_mb and the boost_mb_removed; the whole-grant reversal stays sim.boost_expired
sim.throttledThe SIM is throttled after crossing a usage cap
sim.throttle_clearedThrottling is lifted (cycle rollover or manual clear)
sim.port_out_pin_generatedA port-out PIN is generated for the SIM
sim.port_out_pin_clearedThe SIM's port-out PIN is cleared

SIM inventory events

The pool of unassigned cards an operator loads and activations draw from.

TypeFires when
sim_inventory.importedA batch of cards is loaded into the pool. One event per import, not one per card
sim_inventory.claimedA card is drawn from the pool for a new SIM
sim_inventory.releasedA claimed card goes back into the pool because its SIM's port-in was cancelled or failed at the carrier
sim_inventory.quarantinedA card is taken out of circulation and will not be drawn again. Carries quarantine_reason
sim_inventory.low_stockA claim leaves exactly the low-stock threshold of available cards for that carrier connection and card type. Carries available and threshold. It fires on the crossing only: a pool loaded already below the threshold never fires it

Port events

TypeFires when
port.createdA port-in request record is created
port.submittedThe port-in request is accepted by heliumOS
port.in_progressThe carrier has the port-in in flight
port.resolution_requiredThe 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_expiringA 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.completedThe number transfer completed
port.failedThe port-in failed
port.cancelledThe port-in was cancelled
port.out_validation_requestedThe gaining carrier requested port-out validation
port.out_completedA port-out finalized and the line left heliumOS

Catalog events

The plans and addons an operator sells. Emitted in the same transaction as the catalog write, and the payload is the full public object — an integration mirroring the catalog can upsert straight from the event rather than re-reading it, and hears a price change the moment it is made.

TypeFires when
plan.createdA plan is added to the catalog
plan.updatedA plan's details or price change
plan.deletedA plan is withdrawn from sale. Subscriptions already on it are unaffected
addon.createdAn addon is added to the catalog
addon.updatedAn addon's details or price change
addon.deletedAn addon is withdrawn from sale. Addons already attached to a subscription are unaffected

Usage events

TypeFires when
usage.data_threshold_crossedA configured data-usage threshold is crossed
usage.voice_threshold_crossedA configured voice-usage threshold is crossed
usage.sms_threshold_crossedA configured SMS-usage threshold is crossed
usage.intl_voice_threshold_crossedThe carrier reports the line is running out of, or has used up, its international calling allowance
usage.intl_roaming_threshold_crossedThe carrier reports the line is running out of, or has used up, its international roaming allowance
usage.intl_sms_threshold_crossedThe carrier reports the line is running out of, or has used up, its international texting allowance
usage.tier_changedThe SIM moves between throttle tiers
usage.cycle_rolled_overA billing cycle rolls over and counters reset

Billing events

TypeFires when
invoice.upcomingA pre-renewal notice three days ahead of the next billing cycle (a preview invoice object, status: "upcoming", no id yet). See the note below — it fires for the subscriptions heliumOS bills itself: every sandbox subscription, and the live ones that have moved onto its billing engine
invoice.createdAn invoice is created
invoice.finalizedAn invoice is finalized and ready to collect
invoice.document_readyThe invoice's PDF has been rendered and can be linked to. Fires once per stored document, moments after invoice.finalized — see the note below
invoice.paidAn invoice is paid
invoice.payment_failedA payment attempt on the invoice fails
invoice.voidedAn invoice is voided
invoice.marked_uncollectibleheliumOS gives up collecting on a finalized invoice. The debt stays on the books — this is not the same as voiding it — but the collection ladder stops. Fires whether the write-off happens on heliumOS's own billing engine or is mirrored from the payment processor
payment.succeededA payment succeeds
payment.failedA payment fails
payment.refundedA payment is refunded
payment_method.attachedA payment method is attached to a subscriber
payment_method.updatedA payment method changes for one of two unrelated reasons. It becomes the subscriber's default (previous_attributes carries is_default: false) — the subscriber's own card in their wallet, and its on-file detail, are unchanged. Or its on-file detail (brand, last four digits, expiry) changes — either the card network pushed a replacement PAN or expiry onto the same credential, or the detail was edited directly (previous_attributes carries one or more of card_brand, card_last4, card_exp_month, card_exp_year, holding each field's prior value). Check which keys previous_attributes carries to tell the two apart; the object itself always reflects the payment method's current state either way
payment_method.detachedA payment method is detached from a subscriber
balance.creditedAn operator issues a subscriber a positive account credit — a goodwill credit, a service credit — through POST /v1/subscribers/{id}/balance_credits. Distinct from discount.credit_applied below; the two are never emitted for the same money, so a reconciliation cannot double-count
discount.credit_appliedA recurring discount credit lands on an invoice heliumOS issued itself

Which subscriptions get an invoice.upcoming

Every subscription is billed by one of two things: heliumOS's own billing engine, or the payment processor behind it. That is what decides whether you get a pre-renewal notice.

  • Sandbox subscriptions are billed by heliumOS, always. They get an invoice.upcoming three days before the cycle turns over, and the amount on it is the renewal's own arithmetic — every seat on a family plan, the add-ons falling due on this cycle, and any credit carried over from a mid-cycle change. It is a pre-tax figure: tax is assessed when the invoice is actually raised.
  • Live subscriptions move onto heliumOS's billing engine in stages, a subscription at a time rather than a whole environment at once, so one live environment can hold both kinds. One that has moved gets the same notice as a sandbox subscription. One the payment processor still bills gets none, because the processor, not heliumOS, is the only thing that can say what its next bill will be. Nothing about the payload changes when a subscription moves.

If you want a heads-up on a live subscription today, poll GET /v1/subscriptions/{id}/renewal, which answers the same question on demand.

Emailing the bill: wait for invoice.document_ready

heliumOS does not send email or SMS to your subscribers — you do, on your own channels, with your own wording. Two fields on the invoice are what you send them:

  • hosted_invoice_url — a page your customer can open with no login, safe to put in an email or an SMS.
  • document_url — the PDF of the same bill, on the same link.

Both are signed and valid for 90 days. Read them fresh from the API when you send rather than storing them, and your links are always current; a link that has aged out answers 410 with a plain page telling the reader to ask you for a new one.

The catch is timing. invoice.finalized fires the instant the bill is frozen, and the PDF is rendered moments later — so an invoice fetched on invoice.finalized can still carry document_url: null. invoice.document_ready is the event that says the document exists. Send on that one, and both fields are populated on the invoice object it carries.

Invoices issued before heliumOS began rendering documents have no document_url, and their hosted_invoice_url is the payment provider's own page — which expires on the provider's schedule, not ours.

Dispute events

A cardholder disputed a charge with their bank — a chargeback.

TypeFires when
dispute.openedA cardholder disputes a charge with their bank
dispute.updatedThe dispute's status or reason changes, including an inquiry being reclassified as a fraud chargeback
dispute.closedThe bank reaches its final verdict, won or lost

pSIM order events

TypeFires when
psim_order.paidA paid pSIM order is placed (the card is charged)
psim_order.createdA free pSIM order is placed (no charge)
psim_order.refundedA pSIM order's charge is refunded (or a paid order is canceled)
psim_order.canceledA 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.

SMS events

TypeFires when
sms.receivedA subscriber texts your short code. Payload: the inbound_message. heliumOS does not answer it yet

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.

TypeFires when
cpni.verification.requestedAn agent started a step-up challenge, carries the one-time code
cpni.verification.succeededThe agent entered the correct code
cpni.verification.failedThe challenge locked out after too many attempts
cpni.escalation.requestedAn agent asked a supervisor for access without the subscriber's code, for when the customer can't be reached. Carries the agent's reason
cpni.escalation.approvedA supervisor approved the request, and the agent has access for a limited window. This is the event to tell the subscriber their account was opened without their verification
cpni.escalation.deniedA supervisor turned the request down

The cpni.escalation.* events carry no personal data and no code.

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
    }
  }
}

Team hand-offs: submitted_by

data.object.agent on every cpni.verification.* event is the agent who requested the challenge. When your operator shares verified access across the support team, a teammate can take over the chat and enter the code the subscriber reads back. On cpni.verification.succeeded and cpni.verification.failed, a submitted_by object then names the agent who entered the code (or, on a failure, the wrong codes). It is present only when that agent differs from agent; when the requester entered the code themselves, the field is omitted.

{
  "type": "cpni.verification.succeeded",
  "data": {
    "object": {
      "object": "cpni_verification",
      "id": "cpniv_01HGZX...",
      "status": "verified",
      "agent": { "user_id": "...", "email": "first-agent@operator.example" },
      "submitted_by": { "user_id": "...", "email": "second-agent@operator.example" }
    }
  }
}

The dashboard's verification log records who entered the correct code as Completed by, including when that was the requesting agent.

Referral events

Only fire when the referrals product is switched on for your operator.

TypeFires when
referral.createdA signup was attributed to somebody's referral code. No money has moved yet — this is the event to congratulate the referrer on
referral.reward.earnedA reward step resolved to an amount. data.object is the reward, carrying referral_id, step_index, recipient, amount_cents and the invoice that triggered it. The amount is frozen here and never re-read from the offer
referral.reward.fulfilledThe credit landed on the recipient's account. Carries credit_ref. balance.credited also fires from the credit primitive with kind: "referral" — the two describe the same money from opposite ends, so drive your accounting from one of them, not both
referral.reward.forfeitedA step will never be earned, with reason: the referee cancelled, the invoice that would have counted was refunded, the referrer stopped being a customer, a cap was reached, or the referral was voided
referral.completedEvery reward step on a referral has a final outcome
referral.voidedAn operator reversed a mistaken attribution, with the reason recorded

Payloads carry public ids and no personal data about either party. Changes to a referral offer are operator configuration and are audited in the dashboard rather than broadcast. See the Referrals guide.

Carrier connection events

TypeFires when
carrier_connection.verifiedA scheduled verify call succeeded
carrier_connection.auth_failedA verify call failed authentication
carrier_connection.credentials_expiredThe connection's carrier credentials expired

Payment connection events

Fire only when a scheduled check finds the connection's status has changed; previous_attributes carries the old status.

TypeFires when
payment_connection.verifiedThe payment processor connection is working again (status becomes active)
payment_connection.auth_failedThe processor rejected the connection's credentials
payment_connection.credentials_expiredThe connection's processor credentials expired

Fraud screening events

Fraud screening is enabled per operator. These events only fire for an operator it has been turned on for; ask your heliumOS contact to enable it.

TypeFires when
fraud.evaluatedA signup, account change or POST /v1/fraud/decisions request was screened. Carries the decision, score, reasons and mode, and the screened subject — which includes the email address and phone number, so treat the payload as personal data

Webhook endpoint events

TypeFires when
webhook_endpoint.testYou send a test event to an endpoint from the dashboard. Carries only the endpoint's id

Event types are additive: new types may appear without a major version bump. Handlers should ignore unknown type values.