Skip to content

Sub-Resellers

A sub-reseller is a reseller whose parent_id points to another reseller. Functionally identical to a top-level reseller — they have their own wallet, their own subscribers, their own login URL — except their parent can fund them, withdraw from them, see their numbers, and (since v1.0.546) move subscribers between themselves and the sub in one click.

This page explains how the hierarchy works, what a parent can and cannot do to a sub, and the tradeoffs behind the one-level depth limit.

From either an admin or a parent reseller’s panel:

  • ResellersAdd Reseller
  • Direct URL: /resellers/new

When the parent of the new reseller is non-empty, the new row is a sub-reseller. When parent is empty, it’s a top-level reseller — see Onboarding.

Reseller hierarchy depth is capped at 1: a sub-reseller cannot themselves have sub-resellers. The cap is enforced in the API — a reseller calling POST /resellers with their own ID as parent_id is allowed; a sub-reseller calling the same endpoint gets a 400 “max depth reached”.

Three reasons:

  1. Balance flow stays linear. Parent → sub is one transfer. Parent → sub → sub-sub would be two transfers with double-entry bookkeeping at every level — easy to get wrong, hard to debug.
  2. Permission scope stays flat. “Show me my sub-resellers’ subscribers” is one WHERE clause. “My sub-resellers’ sub-resellers’ subscribers” is recursive — expensive at scale.
  3. No real customer asked. In four years of production deployments, no operator has needed deeper than one level. Resellers who want a deeper tree usually want a separate panel — which is what SaaS-mode does.

When you open the Resellers page as a reseller, you see only your own sub-resellers (you never see other top-level resellers). The list shows:

ColumnWhat it shows
UsernameSub-reseller’s login username.
NameDisplay name (often the company name).
BalanceCurrent wallet balance.
SubscribersCount of subscribers owned by this sub-reseller.
Last loginWhen the sub-reseller last signed in.
ActionsEdit, Transfer, Withdraw, Login as, Delete.

Admins see the full Resellers table (all top-level + all sub-resellers across all parents), with a “Parent” column that links to the parent’s detail page.

ActionAllowed for parentAllowed for adminNotes
View sub-reseller’s profileYesYesIncludes balance, subscriber count, transactions.
Edit sub-reseller’s detailsYesYesName, contact, permission group (within parent’s set).
Top up sub’s balanceYesYesVia Transfer — see below.
Withdraw from sub’s balanceYesYesReturns funds to parent’s own wallet.
Change sub’s parentNoYesRe-parenting is an admin-only privilege.
Change sub’s credit limitNoYesCredit is a trust setting between admin and reseller.
Change sub’s allowed_ipsNoYesSecurity setting — admin only.
Delete the subYes (if empty)YesSub must have zero subscribers and zero balance.
Impersonate / Login asNoYesImpersonation is admin-only — audit-logged.

The “no” rows are enforced server-side in the reseller update handler: the field-whitelist in Update silently drops parent_id, credit, allowed_ips, permission_group, etc. for non-admin callers.

Transfer moves money from the parent’s wallet to the sub-reseller’s wallet:

StepEffect
Parent calls POST /resellers/{sub_id}/transfer with amount=100Parent balance −100, sub balance +100.
Two transaction rows createdOne on parent (type=transfer, amount=−100, target=sub), one on sub (type=transfer, amount=+100).
Audit log entryTransferred $100.00 to <sub_name>.

Withdraw is the reverse — parent pulls funds back from the sub:

StepEffect
Parent calls POST /resellers/{sub_id}/withdraw with amount=50Sub balance −50, parent balance +50.
Two transaction rows createdSub (type=withdraw, amount=−50), parent (type=withdraw, amount=+50, target=sub).
Audit log entryWithdrew $50.00 from <sub_name>.

Both operations enforce direct-child ownership. A parent reseller calling Transfer or Withdraw against a sub that is not their direct child gets a 403 “You can only transfer to your own sub-resellers”. This guard was added as a security audit fix (M6) — without it, any reseller could move money between any two accounts.

See Balance & Transactions for the full list of transaction types.

  1. ResellersAdd Reseller.
  2. Fill in username, password, name, contact. Parent is auto-set to you.
  3. Pick a permission group. Common choice: a constrained group like SALES_LIMITED with no permission to delete subscribers.
  4. Leave starting balance at 0. Click Create.
  5. From the Resellers list, click the new row → Transfer.
  6. Enter 200 (or whatever you’ve agreed) and a note ("May seed funding"). Click Transfer.
  7. Hand the sub-reseller their credentials. They can now sign in and start adding subscribers.

Reclaiming balance from a sub-reseller who under-performed

Section titled “Reclaiming balance from a sub-reseller who under-performed”
  1. Resellers → click the sub → Withdraw.
  2. Enter the amount and a note ("End-of-month settlement").
  3. Confirm. Balance moves from sub to you in one transaction.
  4. If you intend to delete the sub afterwards, first withdraw their full balance, then move their subscribers to yourself via the bulk Transfer Reseller action (see Subscribers), then delete.
  1. Ensure the sub has zero subscribers — the API rejects the delete otherwise. Move them to yourself or a different sub first.
  2. Withdraw any remaining balance back to your own wallet.
  3. Resellers → click sub → Delete.
  4. Soft-delete by default. The username is still reserved (you can’t immediately create another sub with the same name). Use the admin Permanent Delete if you need to free the username — only admins have that button.

Ownership boundaries — what a sub-reseller sees of their parent

Section titled “Ownership boundaries — what a sub-reseller sees of their parent”

A sub-reseller’s view of their parent:

  • They cannot see the parent in any list — the Resellers page filters by parent_id = caller.reseller_id, which excludes the parent itself.
  • They cannot call any endpoint with the parent’s ID — the ownership checks on Update, Transfer, Withdraw all reject “caller is not the owner and not a child”.
  • They see their own balance and transactions. Transactions where the parent is the counter-party show as transfer or withdraw with the parent’s name in the description.

This is the “blind upward” pattern: subs know money comes from somewhere, but they don’t get to introspect the parent.

PermissionEffect
resellers.viewSee your list of sub-resellers.
resellers.createAdd a new sub-reseller.
resellers.editEdit your sub-resellers’ details.
resellers.deleteDelete a sub-reseller (only if empty).
transactions.transferUse Transfer to fund a sub-reseller.
transactions.withdrawPull balance back from a sub-reseller.

Admins have all of these on every reseller in the system.

resellers
├── id
├── user_id → users.id (1:1 — every reseller is also a user row)
├── parent_id → resellers.id (NULL for top-level)
├── balance (NUMERIC — the wallet)
├── credit (NUMERIC — credit limit, default 0)
├── permission_group → permission_groups.id (NULL = full perms)
├── allowed_ips (comma-separated, CIDR or single IP)
└── is_active (BOOL — toggling false blocks login)

Hierarchy queries are written as WHERE parent_id = ? — depth-1 means there’s never a recursive CTE.