Skip to content

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.

https://your-panel-host/api/subscribers

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.

List subscribers with filters + pagination.

Permission: subscribers.view

Query parameters

ParamTypeDefaultDescription
pageint11-indexed page
limitint50Page size, max 500
searchstringMatches username / full_name / phone / mac_address
statusstringactive, inactive, expired, expiring, online, offline
service_idintFilter by plan
reseller_idintFilter by owning reseller (admins only)
nas_idintFilter by last-seen NAS
fup_levelint0–3
sortstringcreated_atcreated_at, username, expiry_date, daily_quota_used
orderstringdescasc 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 }
}
Terminal window
curl https://panel.example.com/api/subscribers?status=online&limit=20 \
-H "Authorization: Bearer eyJhbGc..."

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.

Create a subscriber.

Permission: subscribers.create

Request body

FieldTypeRequiredDescription
usernamestringyesPPPoE login (typically name@realm)
passwordstringyesPlaintext (encrypted at rest, see Security)
service_idintyesFK to services.id
full_namestringno
phonestringnoE.164 recommended for WhatsApp routes
mac_addressstringnoLocked to first auth if mac_binding enabled on service
static_ipstringnoIPv4, must be free in the pool
expiry_datestringnoISO YYYY-MM-DD; defaults to today + service duration
reseller_idintnoAdmin only — defaults to caller’s reseller
region, building, address, nationality, country, notesstringnoCustomer-info fields
pricenumbernoPer-subscriber price override
override_priceboolnoIf true, price takes effect instead of service default

Response — 201 Created — full subscriber object.

Terminal window
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.

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.

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.

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

Terminal window
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.

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).

Reset daily quota counters and FUP tier back to 0. Monthly counters are not touched (use renew for that).

Permission: subscribers.reset_fup

Clear the mac_address so the next PPPoE Start packet rebinds.

Permission: subscribers.reset_mac

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" }

Extend expiry_date by N days without taking a renewal payment.

Permission: subscribers.add_days

Body: { "days": 7, "reason": "promo" }

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”
EndpointEffectBody
/refillReset daily + monthly quota counters, charge refill fee{}
/topup-dataBuy extra GB on top of monthly FUP{ "gb": 10 }
/add-balanceCredit the subscriber’s prepaid wallet{ "amount": 5.00 }

Permission: subscribers.refill_quota

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 }

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).

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.

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": { }
}

Each row documents one action. The permission column is what the caller must hold (admins bypass all permission checks).

actionpayloadPermissionEffect
renew{}subscribers.renewSame as POST /:id/renew per id. Reseller balance debited per id; counters reset; CoA disconnect.
disconnect{}subscribers.disconnectCoA disconnect per id. Idempotent.
enable{}subscribers.inactivatestatus = active.
disable{}subscribers.inactivatestatus = inactive; CoA disconnect.
reset_fup{}subscribers.reset_fupReset daily quota + FUP tier. Monthly counters untouched.
delete{}subscribers.deleteSoft-delete each id.
set_service{ "service_id": 8, "charge_difference": false }subscribers.change_serviceMove every id to the target plan; CoA disconnect.
set_reseller (v1.0.547+){ "reseller_id": 12 }subscribers.transferRe-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.editAssigns / draws from pool. Rejects duplicates already in radreply. CoA disconnect.
set_daily_quota{ "gb": 50 }subscribers.editClamped to 0–10,000 GB.
set_monthly_quota{ "gb": 500 }subscribers.editClamped to 0–10,000 GB.
set_price (v1.0.551+ bounds){ "price": 19.99, "override_price": true }subscribers.editClamped to 0–10,000.
rename{ "old_realm": "@old.lb", "new_realm": "@new.lb" }subscribers.renameSuffix-only rename; charges rename fee per id when configured.
reset_mac{}subscribers.reset_macClear mac_address per id.
refill{}subscribers.refill_quotaReset daily + monthly counters and charge refill fee per id.
{
"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.

{ "success": false, "message": "subscriber not found" }
StatusMeaning
400Validation — see message
401Missing / invalid / blacklisted JWT
403Permission denied, or subscriber not owned by caller’s reseller
404Subscriber id does not exist
409Conflict — duplicate username, IP already used, already renewed
422Action accepted but no-op (e.g. disconnecting an offline user is 200, but renewing an already-renewed-today returns 422)
500DB or NAS-side failure

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.