Endpoints — Sessions
A session is a row in radacct — one PPPoE connect/disconnect cycle. The Sessions API exposes the live view (sessions where acctstoptime IS NULL) plus disconnect controls. Closed/historical sessions are reachable from the same list endpoint with a status filter, and bulk-exported as CSV.
Base URL
Section titled “Base URL”https://your-panel-host/api/sessionsAuthentication
Section titled “Authentication”Authorization: Bearer <jwt> required. Reseller scoping: a reseller sees only sessions belonging to subscribers in their tree. sessions.view_all permission flips a reseller to the admin scope.
The session object
Section titled “The session object”{ "radacctid": 12345678, "username": "user1@example.lb", "acctsessionid": "8a3f9c01", "nasipaddress": "<bng-private>", "nas_id": 2, "nasportid": "ether2", "framedipaddress": "<subscriber-ip>", "callingstationid": "AA:BB:CC:DD:EE:01", "acctstarttime": "2026-05-12T08:14:22Z", "acctstoptime": null, "acctupdatetime": "2026-05-12T11:01:03Z", "acctinputoctets": 1234567890, "acctoutputoctets": 9876543210, "acctsessiontime": 10241, "acctterminatecause": null}The DB column names are PostgreSQL-style: lowercase, no underscores (acctstarttime, not acct_start_time). The API echoes the same names so accounting tooling stays compatible.
GET /api/sessions
Section titled “GET /api/sessions”List sessions.
Permission: sessions.view
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
status | string | active | active (default — open sessions), closed, all |
page | int | 1 | 1-indexed |
limit | int | 50 | Max 500 |
search | string | — | Match on username / framedipaddress / callingstationid |
nas_id | int | — | Filter by NAS that authorised the session |
from | string | — | ISO datetime — sessions started ≥ this |
to | string | — | ISO datetime — sessions started ≤ this |
sort | string | acctstarttime | acctstarttime, acctinputoctets, acctoutputoctets, acctsessiontime |
order | string | desc | asc / desc |
Response — 200 OK
{ "success": true, "data": [ { "radacctid": 12345678, "...": "..." } ], "pagination": { "page": 1, "limit": 50, "total": 1842 }}curl "https://panel.example.com/api/sessions?status=active&limit=100" \ -H "Authorization: Bearer ..."Errors
| Status | message | Cause |
|---|---|---|
| 400 | invalid status, must be active/closed/all | — |
| 403 | sessions.view denied | Caller lacks the permission |
GET /api/sessions/count
Section titled “GET /api/sessions/count”Quick counter — number of currently active sessions in caller’s scope. Used by the Dashboard.
Permission: sessions.view
Response — 200 OK
{ "success": true, "count": 1842 }The count is not cached server-side — it is a SELECT COUNT(*) against radacct WHERE acctstoptime IS NULL. At 25 k subscribers this query runs in ~5 ms with the acctstoptime IS NULL partial index. (This index is ensured at startup via EnsureIndexes.)
GET /api/sessions/:id
Section titled “GET /api/sessions/:id”Return a single session by radacctid.
Permission: sessions.view
curl https://panel.example.com/api/sessions/12345678 \ -H "Authorization: Bearer ..."Errors
| Status | message | Cause |
|---|---|---|
| 404 | session not found | radacctid does not exist |
| 403 | not authorized | Reseller asked for a session outside their scope |
POST /api/sessions/:id/disconnect
Section titled “POST /api/sessions/:id/disconnect”Terminate a live session. The handler:
- Looks up the
nasipaddressfor the session. - Sends a RADIUS CoA Disconnect-Request to that NAS on the NAS’s
coa_port(default 1700 for MikroTik). - Falls back to MikroTik API
/ppp/active/removeif CoA times out (3 s). - Writes an
Acct-Stoprow when the NAS acknowledges, otherwise lets the Stale-Session-Cleanup sweeper close it within 5 minutes.
Permission: subscribers.disconnect
The verb is POST, not DELETE, because the action has side-effects beyond removing a row — the subscriber is disconnected from their PPPoE link.
Request
POST /api/sessions/12345678/disconnectAuthorization: Bearer <jwt>No body.
Response — 200 OK
{ "success": true, "method": "coa", "message": "disconnect request sent"}method is coa when CoA succeeded, api when the MikroTik-API fallback kicked in, or already_closed when the session was already gone by the time we looked. The endpoint is idempotent — repeat calls on a closed session return already_closed with 200.
Errors
| Status | message | Cause |
|---|---|---|
| 404 | session not found | Bad id |
| 503 | NAS unreachable | Both CoA and MikroTik-API attempts failed (rare) |
| 403 | subscribers.disconnect denied | Caller lacks the permission |
GET /api/sessions/export
Section titled “GET /api/sessions/export”Export sessions matching a filter as CSV. Used by the Sessions → Export button and by integration scripts.
Permission: sessions.view
Query parameters — same as GET /api/sessions, plus:
| Param | Type | Default | Description |
|---|---|---|---|
format | string | csv | csv only currently; reserved for xlsx later |
columns | string | (all) | Comma-list to restrict columns |
Response — 200 OK — Content-Type: text/csv with a Content-Disposition: attachment header.
curl -OJ "https://panel.example.com/api/sessions/export?status=closed&from=2026-05-01T00:00:00Z&to=2026-05-12T23:59:59Z" \ -H "Authorization: Bearer ..."Sample CSV:
radacctid,username,nasipaddress,framedipaddress,acctstarttime,acctstoptime,acctsessiontime,acctinputoctets,acctoutputoctets,acctterminatecause12345678,user1@example.lb,<bng-private>,<subscriber-ip>,2026-05-12T08:14:22Z,2026-05-12T12:18:14Z,14632,1234567890,9876543210,User-RequestLimits
- Max 500,000 rows per export. Requests beyond that return
413 too many rows, narrow date range. - Streams the response — does not load the full set into memory. A 500 k-row CSV is ~80 MB and takes 30–60 s to deliver.
- Use
from/toaggressively when working withradacct_archive(anything older than 90 days has been moved there and falls under a separate archival query path).
Stale-session cleanup (background)
Section titled “Stale-session cleanup (background)”The API container runs StaleSessionCleanupService every 5 minutes. It closes sessions where:
acctstoptime IS NULL AND acctupdatetime < NOW() - INTERVAL '30 minutes'This is what stops “ghost” sessions accumulating when a MikroTik reboots without sending Acct-Stop packets. The closed rows get acctterminatecause = 'NAS-Idle-Timeout'. The sweeper also syncs the subscribers.is_online flag — if a user has no open radacct row they are marked offline regardless of what is_online previously said.
Closed rows older than 90 days are archived to radacct_archive by RadAcctArchivalService (runs daily at 03:00). The Sessions API does not automatically union the archive — to query closed sessions older than 90 d, hit radacct_archive directly or use the Reports page which switches to the archive when the range demands it.
Error format
Section titled “Error format”{ "success": false, "message": "session not found" }| Status | Meaning |
|---|---|
| 400 | Validation failure on query params |
| 401 | Missing / expired JWT |
| 403 | Permission denied |
| 404 | Bad id |
| 413 | Export row count exceeded |
| 503 | NAS unreachable on disconnect |
Rate limits
Section titled “Rate limits”300 req/min/IP global. The export endpoint is not rate-limited differently, but the underlying handler holds the DB connection for the duration of the stream — fire-and-forget hundreds of exports in parallel will exhaust the connection pool. One export at a time per integration is sensible.
Related pages
Section titled “Related pages”- Endpoints — Subscribers — sessions are scoped to the same reseller tree
- Stale Session Cleanup — why sessions sometimes stick open
- Reports — aggregated usage views built on top of
radacct+radacct_archive - NAS / Routers —
coa_portconfiguration used by disconnect