How purchases and traffic fit together
There are two ways to give a customer proxy access: buy data for their own order, or let them use part of a parent’s shared pool. Both can look like “add 10 GiB” in your UI, but they are different API workflows.
Six things to keep separate
Section titled “Six things to keep separate”| Resource | In plain language | What it is not |
|---|---|---|
| User | The account that owns credentials and has permissions | A traffic purchase |
| Package | The product: routing, pricing, capabilities, limits and billing cycle | Data already paid for |
| Invoice | The record of a purchase and its payment state | Proof that access is ready just because it exists |
| Order | One user’s access to one package, reused across top-ups | A new subscription record for every payment |
| Data ledger | A purchased bucket with its own remaining bytes and expiration | A complete financial or traffic transaction journal |
| Child quota | The most a managed customer may consume from a shared pool | Reserved data or money transferred to that customer |
A user’s wallet holds money. Ledger balances and child quotas hold bytes. Paying to top up a wallet does not itself buy a proxy package.
Path A: buy independent data
Section titled “Path A: buy independent data”Repeated purchases for the same user and package reuse the order. Finite, expiring purchases create separate buckets; compatible non-expiring top-ups can merge. Your database should keep separate purchase/invoice records even when they point to the same order.
Start with your own billing quickstart or built-in checkout and wallet payments.
Path B: assign a shared-pool quota
Section titled “Path B: assign a shared-pool quota”The parent first needs its own purchased root order for the package. Creating a sub-user alone does not create that pool. Then POST /users/{id}/data/add creates or increases the child’s virtual order, using package_id and data.
- 1 · Before allocationParent: 100 GiB
Purchased data available to the whole order tree.
- 2 · Assign two quotasA: 80 GiB · B: 80 GiB
Parent still has 100 GiB. The 160 GiB of quotas is not reserved.
- 3 · A uses 10 GiBParent: 90 GiB
A has 70 GiB left. B still has 80 GiB. Both depend on the same parent pool.
Access needs personal quota and usable shared data. Either can run out first.
Overcommit is intentional: assigned limits are independent ceilings, not reservations. The API does not cap the sum of those limits at the parent’s remaining data. If your business promises every assigned byte, enforce that reservation policy in your own backend and keep the shared pool funded. Do not implement it as an unprotected read-then-write balance check across concurrent purchases.
When a child uses 10 GiB, the same usage reduces the shared ledger and increases that child’s data_spent. It does not count as 10 GiB of the parent’s own direct traffic. Parent direct traffic also uses the shared pool.
A sub-user is not necessarily a child order
Section titled “A sub-user is not necessarily a child order”There are two independent relationships:
- User parent: who manages the account.
- Order parent: whose purchased pool supplies this package’s traffic.
A managed sub-user can instead receive an independent paid purchase. That order owns its own ledgers and does not use the parent’s pool. Choose the model before provisioning a user/package pair. The allocation endpoint rejects an existing independently purchased order; it is not a conversion API. Do not use an invoice as a way to convert an existing virtual child order either.
Read the right balance
Section titled “Read the right balance”For finite-data packages:
| Field | Purchased root order | Virtual child order |
|---|---|---|
data | Granted/purchased data counter; not the usable balance after expiration | Total assigned quota |
data_spent | Traffic consumed directly by this order’s user, not all children | Traffic consumed by this child |
REST data_remaining | Sum of usable ledger balances | max(data - data_spent, 0) |
REST ledgers | Usable buckets, not complete history | Empty array: the parent owns the buckets |
Do not use root.data - root.data_spent to display a shared pool. That misses both child consumption and expired data. Do not interpret an empty ledger array alone as proof of a virtual order: a purchased order can also have no usable buckets.
Webhook fields are a different contract: shared and personal balances have distinct fields, exhausted finite balances can be negative, and unlimited capacity can be null. Use the webhook field definitions rather than copying REST formulas. Unlimited access can still expire or be disabled.
What can stop access?
Section titled “What can stop access?”Check the user and order state, the child’s personal quota (if virtual), and usable purchased data. Expiration, an empty pool, a depleted child quota, disabled access, credentials, source-IP rules and connection limits are different failures. More child quota cannot repair an empty parent pool.
Usage arrives in batches. A final batch can cross a limit, and dashboards may lag live traffic. Use closed-window analytics for reconciliation, not an instantaneous balance subtraction.
Next: assign and reduce quotas or understand ledger expiration.