docs

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

KeyMeaningSuggested values
signup_platformthe surface the signup happened onios, android, web
signup_app_versionyour client builde.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.

KeyMeaningSuggested values
signup_sourceutm_source, or your ad network's media sourcefacebook, google, Organic
signup_mediumutm_mediumcpc, paid_social, email
signup_campaignutm_campaign, or the ad network's campaignfree text
signup_adutm_content, ad set or ad namee.g. carousel-b
signup_referrerreferring host or deep-link origine.g. news.ycombinator.com
signup_landinglanding path or deep-link valuee.g. /pick-plan
signup_channelyour normalized acquisition bucketorganic, paid_social, paid_search, referral, affiliate, retail, email, direct, unknown
signup_first_touch_atISO8601 instant of that first touche.g. 2026-07-02T09:14:00Z
signup_referral_codethe code of whoever referred themfree text
signup_promotion_codethe promo code they arrived withfree 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

KeyMeaningSuggested values
signup_stepa stable slug for the last screen reachedplan, term, account, verify_email, address, coverage, identity, payment, order_summary, checkout, sim, activate, complete
signup_step_atISO8601 instant that screen was reachede.g. 2026-09-09T14:03:22Z
signup_drop_reasonwhy they stopped, where you knowidv_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.

KeyMeaningSuggested values
signup_planthe plan they chose, checked out or nota plan name
signup_termprepay term in months (not utm_term)1, 12
signup_account_typethe kind of accountconsumer, business, kids
signup_group_roletheir role on a group planpayer, member

Who they are

KeyMeaningSuggested values
signup_devicethe device's marketing namee.g. iPhone 15 Pro
signup_previous_carrierthe carrier they are leavinge.g. Verizon
signup_anonymous_idyour install- or browser-scoped UUID, captured on first launcha UUID
signup_account_created_atISO8601 instant you first created this person's account in your own systeme.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 billable
  • pending_activation — awaiting SIM activation / port completion
  • active — billable, receives usage
  • past_due — payment failure
  • paused — 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_activation until the carrier confirms the MSISDN transfer.
  • Plan change: PATCH /v1/subscriptions/{id} with a new plan_id. Prorations and effective dates are handled server-side per the plan's change_policy.