Resellers
A Reseller is an account that owns a subset of subscribers. They log in with their own credentials, see only their own customers (unless granted subscribers.view_all), and have a balance that is debited every time they create a subscriber or renew one. Sub-resellers nest under a parent reseller, sharing the parent’s permissions inheritance chain.
ProxPanel supports unlimited depth of reseller hierarchy in the database (parent_id self-reference), though most installs stop at two levels: top resellers and their sub-resellers.
How to get here
Section titled “How to get here”Sidebar → Resellers. Direct URL: /resellers.
Requires resellers.view. Resellers viewing this page see only their own sub-resellers, never sibling resellers. Admins see everyone.
Layout
Section titled “Layout”| Section | What it shows |
|---|---|
| Reseller table | Name, Username, Balance, Subscribers count, Parent, Status, Actions |
| Add Reseller button | Create form |
| Stats panel (on detail page) | Total subscribers, Active subscribers, Total revenue |
| Action column | Edit, Top Up, Withdraw, Impersonate, Delete |
The Subscribers count column is loaded with one GROUP BY reseller_id query to avoid N+1 lookups (fixed in v1.0.222).
Form fields
Section titled “Form fields”Account
Section titled “Account”| Field | Notes |
|---|---|
| Username* | Login credential. Must be unique across users. |
| Password* | Set on create; can be re-set in edit. Stored hashed (bcrypt). |
| Optional. Used for forgot-password and notification emails. | |
| Phone | Used by WhatsApp/SMS notifications. |
| Full Name | Display name. Surfaces on subscriber detail (reseller_name) and the Top Resellers card on Dashboard. |
Hierarchy
Section titled “Hierarchy”| Field | Notes |
|---|---|
| Parent Reseller | If set, this reseller is a sub-reseller under another reseller. The parent’s subscribers.view_all ability to see sub-reseller subscribers is wired through reseller_id IN (SELECT id FROM resellers WHERE id = ? OR parent_id = ?). |
| Permission Group | Which permission group governs what this reseller can do. Resellers with no group assigned get all permissions (back-compat default). |
Financial
Section titled “Financial”| Field | Notes |
|---|---|
| Balance | Stored in resellers.balance as a decimal(15,2). Cannot be edited directly — use Top Up / Withdraw. |
| Currency | Display currency for transactions; uses system default from Billing settings unless overridden. |
Security
Section titled “Security”| Field | Notes |
|---|---|
| Allowed IPs | Comma-separated list of IPv4 / CIDR. If non-empty, the login handler rejects logins from any other IP. Useful for office-only resellers. |
| Skip WAN Check | If on, ignores the global WAN reachability check for this reseller’s subscribers. Per-subscriber override also exists. |
Branding & customer-facing
Section titled “Branding & customer-facing”| Field | Notes |
|---|---|
| Custom Branding | Boolean flag enabling per-reseller branding overrides — own logo, colours, support phone. Surfaces on the customer portal when the customer’s subscriber is under this reseller. |
| WhatsApp Enabled | Boolean. When on, this reseller’s communication-rule triggers send via the configured WhatsApp gateway. |
| WhatsApp Instance ID / API Key | Per-reseller Ultramsg or Zender API credentials. Optional — falls back to system-wide settings if blank. |
Balance & transactions
Section titled “Balance & transactions”Every monetary action creates a transactions row:
| Type | When |
|---|---|
new | First-time subscriber creation (deducted from reseller) |
renewal | Subscription renewal |
change_service / service_change | Service change with prorating |
static_ip, addon | Add-on charges |
refill, data_topup | Customer data top-ups |
prepaid_card | Prepaid card redemption |
subscriber_topup, subscriber_purchase | Other revenue |
reset_fup | Mid-cycle FUP reset (when services.reset_price > 0) |
rename | Username rename fee (if configured) |
transfer | Admin moves balance between resellers (no revenue) |
withdraw | Reseller cashes out balance (no revenue) |
refund | Reverse a transaction |
Income-side types (everything except transfer, withdraw, refund) are summed for the Total Income card on the Dashboard and for Reports → Revenue.
Top Up
Section titled “Top Up”The Top Up action adds money to the reseller’s balance. Admin enters an amount and optional note; the handler:
- Inserts a
transactionsrow with positiveamountand typetransfer(orrefilldepending on context). - Increments
resellers.balance. - Writes an audit log.
Withdraw
Section titled “Withdraw”The opposite — removes money from balance. Refused if balance would go negative.
Auto-deduction
Section titled “Auto-deduction”When a reseller creates a subscriber or runs a Renew, the system:
- Checks balance ≥ subscriber price.
- If not, refuses the operation.
- If yes, decrements balance, inserts a transaction with negative amount.
Bulk Renew handles “balance ran out mid-batch” by silently skipping remaining subscribers and reporting the partial count back.
Impersonate
Section titled “Impersonate”The Impersonate button generates a one-time JWT for the reseller and switches the admin’s session to the reseller’s view. Used for troubleshooting “why can’t reseller X see this?” tickets.
What it does:
- Builds a fresh JWT with
user_id = reseller's user_id(not the admin’s user ID) anduser_type = reseller. - Calls back to the frontend with a redirect that stores the new token in the same Zustand flat-storage format as a regular login (fixed in v1.0.220 — earlier versions stored a nested
{ state: {...} }shape that the auth store couldn’t read, leaving the impersonate user with full admin permissions). - The frontend reloads and now sees the panel exactly as the reseller does, with the reseller’s permission group enforced.
To exit impersonation, log out and log back in as admin.
Reseller deactivation
Section titled “Reseller deactivation”Setting is_active = false on a reseller blocks login. This was previously not enforced (deactivated resellers could still log in until v1.0.220) — now checked in three places:
- Login handler — refuses authentication.
- Auth middleware — every request validates
is_activeis still true. - Reseller edit handler — also syncs the underlying
users.is_activefield so the user-management page reflects the state.
Sub-reseller scoping
Section titled “Sub-reseller scoping”A reseller with sub-resellers under them sees a unified subscriber list of their own customers + all sub-resellers’ customers. The SQL pattern used throughout the codebase:
WHERE reseller_id IN ( SELECT id FROM resellers WHERE id = ? OR parent_id = ?)(Both ? placeholders are the calling reseller’s ID.) This is one level deep — three-level hierarchies need a recursive CTE which is on the roadmap.
NAS assignment
Section titled “NAS assignment”A reseller’s accessible NAS list comes from the reseller_nas join table. If empty, the reseller sees no NAS (and can’t create subscribers without one). Manage assignments from the reseller’s detail page → NAS Assignments tab.
Branding overrides
Section titled “Branding overrides”When Custom Branding is on, this reseller’s customers see:
- Reseller’s own company name on invoices and portal.
- Reseller’s logo (uploaded to
reseller_branding). - Reseller’s primary colour.
- Reseller’s support phone / WhatsApp number in the portal.
When off, customers see the global system branding from Settings → Branding.
Common workflows
Section titled “Common workflows”Onboard a new reseller with $500 starting balance
Section titled “Onboard a new reseller with $500 starting balance”- Resellers → Add Reseller.
- Fill username, password, name, email.
- Save (balance starts at 0).
- From the new row click Top Up → enter
500, note “Onboarding deposit”. - Optionally: open the reseller, NAS Assignments → tick the routers they should access, Permission Group → assign the appropriate role.
Diagnose “the reseller is missing a button”
Section titled “Diagnose “the reseller is missing a button””- From the Resellers page, click Impersonate on the affected reseller.
- Panel reloads in their view.
- Confirm whether the button is missing because of permission, or some other rendering issue.
- If missing because of permission — log out, return to admin, edit their Permission Group to add the needed permission.
- Tell the reseller to press F5 — permissions refresh on next page load via
/api/auth/me.
Migrate a reseller’s customers to a sub-reseller
Section titled “Migrate a reseller’s customers to a sub-reseller”- Open Subscribers, filter by the current reseller.
- Select all rows.
- Change Bulk → Set Reseller. Pick the target sub-reseller.
- The bulk handler verifies caller authority: only own children resellers are valid targets (security check at line 2993 of subscriber.go). Anything else returns a refused-action log line.
Permissions
Section titled “Permissions”| Permission | Effect |
|---|---|
resellers.view | Page loads. |
resellers.create | Add Reseller button. |
resellers.edit | Edit + Top Up + Withdraw buttons. |
resellers.delete | Delete (refused if balance non-zero or active subscribers exist). |
resellers.impersonate | Impersonate button. |
transactions.view_all | Reseller can see ALL transactions, not just their own. |
Related pages
Section titled “Related pages”- Users & Permissions — the permission groups assigned here are defined there.
- Subscribers — owned by resellers; reseller filter dropdown lives here.
- NAS / Routers —
reseller_nasassignments restrict which routers a reseller sees. - Billing — transactions table this page writes into.
- Reports — revenue and top-reseller reports based on
transactions. - Communication — WhatsApp gateway config lives here.