Subscribers & subscriptions
A subscriber is the end-user record you create on behalf of your operator. A subscription links a subscriber to a plan and is the billable unit — one subscriber can hold multiple subscriptions over their lifetime.
Subscribers
POST /v1/subscribers
email, name, phone?, metadata?
Subscribers are environment-scoped. The same email can exist in
sandbox and live independently. Subscribers have addresses (shipping
- service) attached via
POST /v1/subscribers/{id}/addresses.
Subscribers are records, not auth principals. End-users never hold a heliumOS token. They authenticate with your mobile app, and your backend calls heliumOS server-to-server.
POST /v1/subscribers/:subscriber_id/payment_sessions mints the
short-lived Stripe PaymentSheet bundle for the mobile on-device
card-add flow; it has no admin surface.
Metadata merges on update
PATCH /v1/subscribers/{id} merges metadata into what is already
stored, the way Stripe does. A request that sends one key leaves every
other key alone, a key sent as null is deleted, and "metadata": null
clears the whole map. A nested object under a key is replaced wholesale
rather than merged, so a nested structure can still be replaced.
That is what makes the attribution below workable: your client can PATCH the current screen on every screen without wiping what the first one recorded.
Signup attribution
metadata is free-form, but the dashboard's subscribers report reads a
set of conventional keys to split the signup funnel by where people came
from, what they picked, and how far they got. Send them and the splits
appear; leave them out and the report says plainly that they are not
being recorded.
One rule runs through all of it: report where the client IS, never where it is in a sequence. Send a stable screen slug and whatever attribution you hold. Ordering, first-versus-last-touch policy and channel normalization are decided server-side, so you can reorder your signup flow without shipping a client release.
The surface
| Key | Meaning | Suggested values |
|---|---|---|
signup_platform | the surface the signup happened on | ios, android, web |
signup_app_version | your client build | e.g. 4.2.9 |
Where they came from — first touch. Send signup_last_source,
signup_last_medium, signup_last_campaign and signup_last_channel
alongside these if you track last touch as well; the two sets travel
separately and neither is inferred from the order you write them in.
| Key | Meaning | Suggested values |
|---|---|---|
signup_source | utm_source, or your ad network's media source | facebook, google, Organic |
signup_medium | utm_medium | cpc, paid_social, email |
signup_campaign | utm_campaign, or the ad network's campaign | free text |
signup_ad | utm_content, ad set or ad name | e.g. carousel-b |
signup_referrer | referring host or deep-link origin | e.g. news.ycombinator.com |
signup_landing | landing path or deep-link value | e.g. /pick-plan |
signup_channel | your normalized acquisition bucket | organic, paid_social, paid_search, referral, affiliate, retail, email, direct, unknown |
signup_first_touch_at | ISO8601 instant of that first touch | e.g. 2026-07-02T09:14:00Z |
signup_referral_code | the code of whoever referred them | free text |
signup_promotion_code | the promo code they arrived with | free text |
The two code keys are reported as a Referral / Promo code / Neither
split, never as a list of the codes themselves.
signup_first_touch_at gets no split of its own — it is a duration, not
a category. The report reads it as the consideration window: the
median and p90 gap between someone's first touch and the moment they
started signing up. Send it and the subscribers report can say how long
people think about it before they begin, over the people you told it
about.
How far they got
| Key | Meaning | Suggested values |
|---|---|---|
signup_step | a stable slug for the last screen reached | plan, term, account, verify_email, address, coverage, identity, payment, order_summary, checkout, sim, activate, complete |
signup_step_at | ISO8601 instant that screen was reached | e.g. 2026-09-09T14:03:22Z |
signup_drop_reason | why they stopped, where you know | idv_failed, address_invalid, payment_declined, plan_unavailable, coverage_unavailable, abandoned |
A step slug is a screen identity, not an ordinal — no step_1, no
step_index, no "of 5". The report works the funnel order out for
itself, from the median time between the signup starting and
signup_step_at, over everyone whose last screen was that one. Send
both keys and the drop-off list is in flow order; reorder your flow and
it re-sorts on its own. Extend the vocabulary by adding slugs, never by
renaming one.
What they picked — known before checkout, which is the point: these cover the people who never checked out.
| Key | Meaning | Suggested values |
|---|---|---|
signup_plan | the plan they chose, checked out or not | a plan name |
signup_term | prepay term in months (not utm_term) | 1, 12 |
signup_account_type | the kind of account | consumer, business, kids |
signup_group_role | their role on a group plan | payer, member |
Who they are
| Key | Meaning | Suggested values |
|---|---|---|
signup_device | the device's marketing name | e.g. iPhone 15 Pro |
signup_previous_carrier | the carrier they are leaving | e.g. Verizon |
signup_anonymous_id | your install- or browser-scoped UUID, captured on first launch | a UUID |
signup_account_created_at | ISO8601 instant you first created this person's account in your own system | e.g. 2024-03-11T16:40:05Z |
signup_account_created_at gets no split of its own. It tells a
returning customer apart from a new signup: when the account is more
than a day older than the subscriber record, and the person holds no
subscription of their own, the subscribers report leaves them out. Send
it when you create a subscriber for someone who already had an account
with you, so a login is not counted as a signup. If they go on to buy,
they count.
Create the subscriber at the first screen with everything you hold, then
PATCH /v1/subscribers/{id} with the current screen as the person
advances, so a signup that stops before checkout says where it stopped:
{
"email": "sam@example.com",
"metadata": {
"signup_platform": "ios",
"signup_app_version": "4.2.9",
"signup_source": "facebook",
"signup_medium": "paid_social",
"signup_channel": "paid_social",
"signup_campaign": "back-to-school-2026",
"signup_first_touch_at": "2026-07-02T09:14:00Z",
"signup_plan": "Unlimited",
"signup_device": "iPhone 15 Pro",
"signup_step": "plan",
"signup_step_at": "2026-09-09T14:03:22Z"
}
}
Then, one screen later:
{
"metadata": {
"signup_step": "payment",
"signup_step_at": "2026-09-09T14:09:47Z"
}
}
The merge is why the second request is safe: everything the first one recorded is still there.
No PII in these keys — no email, no phone number, no address, no name. They exist to describe a signup, not the person.
Account lock (SIM-swap / port-out protection)
Every subscriber carries a locked_status, either unlocked (the
default) or user_initiated_lock. Set it with the locked boolean
on create or update:
PATCH /v1/subscribers/{id}
{ "locked": true } # lock; false to unlock
Relay your subscriber's "lock my account" request here from your
backend using your operator API key. In the operator dashboard, only
the owner role can toggle the lock; other roles that can edit a
profile cannot lock or unlock. While a
subscriber is user_initiated_lock, the platform refuses the carrier
number-portability and SIM-mobility actions an account takeover would
use, returning 409 subscriber_locked:
- SIM swap (
POST /v1/sims/{id}/swap) - MSISDN change (
POST /v1/sims/{id}/change_msisdn) - SLO product changes (
POST /v1/sims/{id}/products) - Setting, generating, or clearing the port-out PIN
(
/v1/sims/{sim_id}/port_out_pin)
Everything else, including profile edits, plan and add-on changes,
WPS changes (POST /v1/sims/{id}/wps, which only selects the carrier's
pooled-data billing tier), billing, and port-in, is unaffected: a locked account can still be
managed, it just can't have its number or SIM moved off. Locking and
unlocking fire the subscriber.locked / subscriber.unlocked webhook
events.
Subscriptions
POST /v1/subscriptions
subscriber_id, plan_id, addons?, activation?
The subscription lifecycle is:
draft— created, not yet billablepending_activation— awaiting SIM activation / port completionactive— billable, receives usagepast_due— payment failurepaused— operator-initiated hold on BILLING: no renewal charge until resumed, and service keeps running (suspend the SIMs to stop service)canceled— terminal
Transitions fire subscription.* webhook events; see
Webhooks and the Event catalog.
Common flows
- New activation: create subscriber → create subscription → order SIM → activate.
- Port-in: create subscriber → create subscription → submit port
request; the subscription stays
pending_activationuntil the carrier confirms the MSISDN transfer. - Plan change:
PATCH /v1/subscriptions/{id}with a newplan_id. Prorations and effective dates are handled server-side per the plan'schange_policy.