Endpoints — Subscribers
Subscribers are the central object in ProxPanel. Every PPPoE user, every monthly invoice, every RADIUS session traces back to one row in the subscribers table. This page documents every verb the JWT API exposes against /api/subscribers.
Base URL
Section titled “Base URL”https://your-panel-host/api/subscribersAuthentication
Section titled “Authentication”Every endpoint here requires Authorization: Bearer <jwt>. See Authentication. The action endpoints (/renew, /disconnect, etc.) require specific permissions — listed per-endpoint below.
Resellers see only subscribers they own (or their descendants own). Admins see everything. The subscribers.view_all permission flips a reseller into the same scope as an admin for read-only.
GET /api/subscribers
Section titled “GET /api/subscribers”List subscribers with filters + pagination.
Permission: subscribers.view
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | 1-indexed page |
limit | int | 50 | Page size, max 500 |
search | string | — | Matches username / full_name / phone / mac_address |
status | string | — | active, inactive, expired, expiring, online, offline |
service_id | int | — | Filter by plan |
reseller_id | int | — | Filter by owning reseller (admins only) |
nas_id | int | — | Filter by last-seen NAS |
fup_level | int | — | 0–3 |
sort | string | created_at | created_at, username, expiry_date, daily_quota_used |
order | string | desc | asc or desc |
Response — 200 OK
{ "success": true, "data": [ { "id": 102, "username": "user1@example.lb", "full_name": "Customer One", "phone": "+96170111111", "mac_address": "AA:BB:CC:DD:EE:01", "service_id": 4, "reseller_id": 7, "status": "active", "is_online": true, "ip_address": "<subscriber-ip>", "fup_level": 0, "daily_quota_used": 1234567890, "expiry_date": "2026-06-01", "created_at": "2025-12-01T10:00:00Z" } ], "pagination": { "page": 1, "limit": 50, "total": 1284 }}curl https://panel.example.com/api/subscribers?status=online&limit=20 \ -H "Authorization: Bearer eyJhbGc..."GET /api/subscribers/:id
Section titled “GET /api/subscribers/:id”Return one subscriber, including service, reseller, decrypted password, and counters.
Permission: subscribers.view
Response — 200 OK
{ "success": true, "data": { "id": 102, "username": "user1@example.lb", "...": "..." }, "password": "decrypted-plain-password", "daily_breakdown": [ { "date": "2026-05-11", "download_bytes": 1.2e9 } ]}Errors: 404 subscriber not found, 403 not authorized.
POST /api/subscribers
Section titled “POST /api/subscribers”Create a subscriber.
Permission: subscribers.create
Request body
| Field | Type | Required | Description |
|---|---|---|---|
username | string | yes | PPPoE login (typically name@realm) |
password | string | yes | Plaintext (encrypted at rest, see Security) |
service_id | int | yes | FK to services.id |
full_name | string | no | — |
phone | string | no | E.164 recommended for WhatsApp routes |
mac_address | string | no | Locked to first auth if mac_binding enabled on service |
static_ip | string | no | IPv4, must be free in the pool |
expiry_date | string | no | ISO YYYY-MM-DD; defaults to today + service duration |
reseller_id | int | no | Admin only — defaults to caller’s reseller |
region, building, address, nationality, country, notes | string | no | Customer-info fields |
price | number | no | Per-subscriber price override |
override_price | bool | no | If true, price takes effect instead of service default |
Response — 201 Created — full subscriber object.
curl -X POST https://panel.example.com/api/subscribers \ -H "Authorization: Bearer ..." \ -H "Content-Type: application/json" \ -d '{"username":"new1@example.lb","password":"s3cret","service_id":4,"full_name":"New Customer"}'Errors: 409 username already exists, 400 service_id not found, 403 reseller has no service-limit for this plan.
PUT /api/subscribers/:id
Section titled “PUT /api/subscribers/:id”Update any subset of the create fields. Whitelisted server-side — unknown fields are silently dropped.
Permission: subscribers.edit
If service_id changes, the handler clears the existing Framed-IP-Address in radreply and disconnects the active session so the next reconnect pulls an IP from the new service’s pool.
If password changes, the new plaintext is encrypted with the license-server-issued AES-GCM key before storage.
Response — 200 OK — updated subscriber.
DELETE /api/subscribers/:id
Section titled “DELETE /api/subscribers/:id”Soft-delete (sets deleted_at). The username stays unique-by-partial-index, so the same username can be created again later.
Permission: subscribers.delete
Hard delete is DELETE /api/subscribers/:id/permanent and is admin only.
POST /api/subscribers/:id/renew
Section titled “POST /api/subscribers/:id/renew”Charge the reseller balance, extend expiry_date by the service duration, reset daily/monthly counters, and disconnect+reconnect to apply RADIUS attribute changes.
Permission: subscribers.renew
curl -X POST https://panel.example.com/api/subscribers/102/renew \ -H "Authorization: Bearer ..."Response includes the new expiry_date and the resulting transaction_id so callers can render the receipt.
Errors: 402 insufficient reseller balance, 409 subscriber already renewed today.
POST /api/subscribers/:id/disconnect
Section titled “POST /api/subscribers/:id/disconnect”Send a RADIUS CoA Disconnect-Request to the NAS that owns the live session. Falls back to MikroTik API kick if CoA times out.
Permission: subscribers.disconnect
Returns 200 even if the user is already offline (idempotent).
POST /api/subscribers/:id/reset-fup
Section titled “POST /api/subscribers/:id/reset-fup”Reset daily quota counters and FUP tier back to 0. Monthly counters are not touched (use renew for that).
Permission: subscribers.reset_fup
POST /api/subscribers/:id/reset-mac
Section titled “POST /api/subscribers/:id/reset-mac”Clear the mac_address so the next PPPoE Start packet rebinds.
Permission: subscribers.reset_mac
POST /api/subscribers/:id/rename
Section titled “POST /api/subscribers/:id/rename”Atomically change username (including the realm) in subscribers, radcheck, radreply, radacct, and transactions. Charges the configured rename fee.
Permission: subscribers.rename
Body: { "new_username": "newname@example.lb" }
POST /api/subscribers/:id/add-days
Section titled “POST /api/subscribers/:id/add-days”Extend expiry_date by N days without taking a renewal payment.
Permission: subscribers.add_days
Body: { "days": 7, "reason": "promo" }
POST /api/subscribers/:id/change-service
Section titled “POST /api/subscribers/:id/change-service”Move the subscriber to a different service plan. Pro-rates the difference between the unused portion of the current plan and the new plan’s price. Disconnects so the new IP-pool / speed attributes apply.
Permission: subscribers.change_service
Body: { "service_id": 8, "charge_difference": true }
GET /api/subscribers/:id/calculate-change-service-price returns the prorated amount without committing — used by the UI to show the operator before they confirm.
POST /api/subscribers/:id/activate · /deactivate
Section titled “POST /api/subscribers/:id/activate · /deactivate”Toggle status between active and inactive. Deactivating triggers a CoA disconnect.
Permission: subscribers.inactivate
POST /api/subscribers/:id/refill · /topup-data · /add-balance
Section titled “POST /api/subscribers/:id/refill · /topup-data · /add-balance”| Endpoint | Effect | Body |
|---|---|---|
/refill | Reset daily + monthly quota counters, charge refill fee | {} |
/topup-data | Buy extra GB on top of monthly FUP | { "gb": 10 } |
/add-balance | Credit the subscriber’s prepaid wallet | { "amount": 5.00 } |
Permission: subscribers.refill_quota
POST /api/subscribers/:id/ping
Section titled “POST /api/subscribers/:id/ping”Run a ping against the subscriber’s current ip_address from the NAS (not from the API container). Returns RTT, loss, and the raw output.
Permission: subscribers.ping
Body: { "count": 4, "size": 64 }
GET /api/subscribers/:id/torch
Section titled “GET /api/subscribers/:id/torch”Live-traffic snapshot via MikroTik /tool/torch. Returns per-connection rates in bits/sec for download and upload.
Permission: subscribers.torch
Query: ?duration=3 (seconds, max 10).
GET /api/subscribers/:id/bandwidth
Section titled “GET /api/subscribers/:id/bandwidth”Returns 24-hour, 30-day, and per-session bandwidth time-series from radacct. Used by the subscriber-edit graph tab.
Permission: subscribers.view_graph
GET / POST / PUT / DELETE /api/subscribers/:id/bandwidth-rules
Section titled “GET / POST / PUT / DELETE /api/subscribers/:id/bandwidth-rules”Per-subscriber bandwidth-rule overrides. The list endpoint returns all rules; create adds one; update edits one; delete removes one.
Permission: subscribers.view (read), subscribers.edit (write)
See Bandwidth Rules for the rule shape.
POST /api/subscribers/bulk-action
Section titled “POST /api/subscribers/bulk-action”The workhorse endpoint. One call applies one action to a list of subscriber ids.
Permission: caller must be admin or reseller; per-action permission is checked inside the handler.
Request body
{ "ids": [101, 102, 103], "action": "renew", "payload": { }}Action values
Section titled “Action values”Each row documents one action. The permission column is what the caller must hold (admins bypass all permission checks).
action | payload | Permission | Effect |
|---|---|---|---|
renew | {} | subscribers.renew | Same as POST /:id/renew per id. Reseller balance debited per id; counters reset; CoA disconnect. |
disconnect | {} | subscribers.disconnect | CoA disconnect per id. Idempotent. |
enable | {} | subscribers.inactivate | status = active. |
disable | {} | subscribers.inactivate | status = inactive; CoA disconnect. |
reset_fup | {} | subscribers.reset_fup | Reset daily quota + FUP tier. Monthly counters untouched. |
delete | {} | subscribers.delete | Soft-delete each id. |
set_service | { "service_id": 8, "charge_difference": false } | subscribers.change_service | Move every id to the target plan; CoA disconnect. |
set_reseller (v1.0.547+) | { "reseller_id": 12 } | subscribers.transfer | Re-assign to another reseller. Target must be in caller’s whitelist — admin can target any reseller; a parent reseller only their own descendants. |
set_static_ip (v1.0.551+ IPv4) | { "ip": "<subscriber-ip>" } or { "pool_name": "STATIC" } | subscribers.edit | Assigns / draws from pool. Rejects duplicates already in radreply. CoA disconnect. |
set_daily_quota | { "gb": 50 } | subscribers.edit | Clamped to 0–10,000 GB. |
set_monthly_quota | { "gb": 500 } | subscribers.edit | Clamped to 0–10,000 GB. |
set_price (v1.0.551+ bounds) | { "price": 19.99, "override_price": true } | subscribers.edit | Clamped to 0–10,000. |
rename | { "old_realm": "@old.lb", "new_realm": "@new.lb" } | subscribers.rename | Suffix-only rename; charges rename fee per id when configured. |
reset_mac | {} | subscribers.reset_mac | Clear mac_address per id. |
refill | {} | subscribers.refill_quota | Reset daily + monthly counters and charge refill fee per id. |
Response
Section titled “Response”{ "success": true, "data": { "succeeded": [101, 102], "failed": [ { "id": 103, "reason": "insufficient reseller balance" } ] }}The endpoint returns 200 even when some ids fail. Inspect failed before declaring victory.
Error format
Section titled “Error format”{ "success": false, "message": "subscriber not found" }| Status | Meaning |
|---|---|
| 400 | Validation — see message |
| 401 | Missing / invalid / blacklisted JWT |
| 403 | Permission denied, or subscriber not owned by caller’s reseller |
| 404 | Subscriber id does not exist |
| 409 | Conflict — duplicate username, IP already used, already renewed |
| 422 | Action accepted but no-op (e.g. disconnecting an offline user is 200, but renewing an already-renewed-today returns 422) |
| 500 | DB or NAS-side failure |
Rate limits
Section titled “Rate limits”300 req/min/IP on the JWT surface. The bulk-action endpoint counts as one request regardless of ids — large bulk operations should be issued in fewer calls, not many small ones.
CoA-based actions (disconnect, renew, etc.) take 30–300 ms per id on a healthy MikroTik. A bulk-disconnect of 5,000 users will take 5–15 minutes; the request times out at 30 s, but the worker continues in the background and updates the bulk_jobs audit row.
Related pages
Section titled “Related pages”- Endpoints — Services — plans you assign to subscribers
- Endpoints — Sessions — live RADIUS sessions
- Authentication — how to acquire the JWT used here
- External API Keys — non-interactive equivalent surface