Licensing

What phones home

Every field your deployment sends to the Hub, why it sends it, and what it never sends.

What phones home

Your deployment makes three kinds of call to the Hub and no others. This page lists what is in each one, field by field. If you are reviewing this before a purchase, this is the page to read, and everything on it can be confirmed by capturing your own egress.

The three calls

CallWhenEndpoint
ActivationOnce, at enrollmentPOST /v1/deployments/activate
RenewalOn a schedule, well before the license expiresPOST /v1/licenses/{jti}/renew
HeartbeatEvery PHONEHOME_INTERVAL (default 15 minutes, floor 60 seconds)POST /v1/deployments/phone-home

All three go to activation.parentUrl, over HTTPS, outbound from your namespace.

The heartbeat

The body is a single usage object. These are all of the fields:

{
  "usage": {
    "collected_at": "2026-07-12T09:15:00Z",
    "sims_active": 1284,
    "sims_total": 1509,
    "subscriptions_active": 977,
    "users_total": 43,
    "orders_30d": 612,
    "data_bytes_30d": 4180331495424,
    "version": "1.42.0"
  }
}

Each number is one COUNT or SUM your own deployment runs against its own database, immediately before it sends:

FieldWhere it comes from
sims_totalselect count(*) from public.sim
sims_activeselect count(*) from public.sim where state = 'active'
subscriptions_activeselect count(*) from public.subscription where state = 'active'
users_totalselect count(*) from auth.users
orders_30dselect count(*) from public."order" where created_at > now() - interval '30 days'
data_bytes_30dselect coalesce(sum(usage_bytes), 0) from public.sim_data_usage where timestamp > now() - interval '30 days'
versionThe version of the API binary you are running
collected_atWhen the counters were read, UTC

They are counts and totals. There are no identifiers in them, no rows, and no sample of the underlying data.

We ask for them because your subscription is priced on volume, and we have no way to see inside your cluster. If you would rather we billed you on numbers you send us than on numbers we take from you, this is the shape that makes that possible.

Collection never blocks the heartbeat. If a counter query fails, the heartbeat still goes out with no usage body at all, and the Hub records the check-in as normal.

Cadence. The platform reads PHONEHOME_INTERVAL, a Go duration. Set it through the chart's env passthrough, --set api.env.PHONEHOME_INTERVAL=5m. Unset means the built-in default of 15 minutes, floored at 60 seconds. There is no separate knob for the usage report: it rides the same tick.

What the heartbeat does not send

Nothing about your infrastructure. No node names, no pod names, no namespace layout, no cluster version, no cloud provider, no CPU, no memory, no disk, no network topology.

No logs. No stack traces. No error reports. No configuration, no environment variables, no connection strings.

No personal data belonging to your customers. No names, no email addresses, no phone numbers, no ICCIDs, no order contents, no addresses, no payment details. users_total is a count of rows in auth.users. It does not carry who they are.

No catalog, no pricing, no revenue figures.

What is on the request itself

The heartbeat is authenticated by proof of possession, so it carries these headers:

  • Authorization: DPoP <your license token>
  • DPoP: <a proof JWT signed with your private key>. The proof binds the HTTP method, the request URL, a fresh identifier, and a digest of the body.
  • Content-Digest: <sha-256 of the body>

The license token in that header contains the identifiers the Hub uses to find your row: the license id, your deployment id, and the expiry. Those are values we issued to you in the first place.

Because this is an HTTPS request that we receive, we necessarily see where it came from. The Hub records the source IP address and the User-Agent string against the timestamp of each check-in, the same way any web server records a request. That is the complete set of things we observe that your deployment did not choose to send.

What the response carries

{
  "deployment_id": "8c1f...",
  "last_phonehome_at": "2026-07-12T09:15:01Z",
  "lease_status": "active"
}

lease_status is the one field that changes anything. It is active or revoked. This is how your deployment learns it has been revoked: it asks, on its own schedule, and the answer tells it. Nothing is ever pushed to you. A revoked answer makes the deployment park itself, which is covered in Revoke and recover.

The other channel for the same signal is the renew call, which answers 410 Gone for a revoked license. Whichever the deployment reaches first, the effect is the same.

The other two calls

Activation posts the activation code and the public half of the key the agent just generated, along with a self-signed proof that it holds the private half. It sends no business data at all. The Hub answers with your license.

Renewal posts one field, confirmation_jkt: the thumbprint of the new public key the agent wants the successor license bound to. The request is signed with the current key. It sends no business data either. The Hub answers with the successor license.

Turning it off

You cannot, and you should not want to. The heartbeat is the same channel that tells your deployment its license is still good. A deployment that stops calling home stops learning anything, including that it is fine, and eventually its license expires and it stops serving. Slowing it down is supported: raise PHONEHOME_INTERVAL (--set api.env.PHONEHOME_INTERVAL=1h). Revocation then takes up to that interval to reach you, which is the tradeoff you are making.

Copyright © 2026