# Sessions - X-Radius API

> Base URL: https://x-radius.com/api/v1
> Auth: Authorization: Bearer xrt_...  (a manager API token)
> Envelope: {"data": ...}; lists add {"meta":{page,page_size,total,has_next}}
> Errors: {"error":{"code","message","request_id"}} - branch on code, never on message
> Timestamps: yyyy-MM-dd HH:mm:ss, UTC
> Money: a bare JSON number in major units, with an ISO-4217 currency code beside it
> Idempotency: redeem and activate endpoints take a client-supplied request_id (UUID)
>
> This page: https://x-radius.com/docs/api/sessions
> Every group: https://x-radius.com/llms.txt

11 endpoints in 1 resource groups. 8 carry a hand-written reference entry with examples; the remaining 3 are generated from the running router and carry method, path, authentication, permission and rate-limit bucket, but no request or response example.

### List live sessions

`GET /api/v1/admin/sessions`

Sessions with no stop time, paginated with the standard list parameters. A non-admin caller sees only sessions belonging to their own manager subtree, injected server-side.

- Authentication: manager session (JWT) or API token
- Permission: `prm_report_sessions` (Sessions report)
- Risk: read

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `q` | query | string | no | Searches username, first and last name, framed IP, device MAC and NAS name. |
| `filter[nas_id]` | query | integer | no | Sessions on one registered NAS. |
| `filter[kind]` | query | enum | no | subscriber, guest, voucher or unknown. |
| `filter[has_ip]` | query | boolean | no | Sessions that were assigned a framed IP. |
| `filter[duration_band]` | query | string | no | Duration bucket. The value "stale" means no accounting update for longer than the tenant's idle window, which is the practical definition of a ghost session. |
| `filter[expired]` | query | boolean | no | Sessions whose owner's subscription has run out but who are still connected. |

#### Response — 200 OK

```json
{
  "data": [
    {
      "id": 8821904,
      "acct_session_id": "81b0c4e2",
      "username": "ahmed",
      "user_id": 4711,
      "kind": "subscriber",
      "nas_ip": "10.20.0.1",
      "nas_id": 3,
      "nas_name": "cairo-edge-1",
      "vendor": "mikrotik",
      "framed_ip": "10.64.12.51",
      "calling_station_id": "AA:BB:CC:DD:EE:FF",
      "start_time": "2026-09-20 07:11:02",
      "update_time": "2026-09-20 09:06:02",
      "session_time": 6900,
      "input_octets": 412000000,
      "output_octets": 5100000000,
      "profile_name": "Home 20M",
      "usage": { "percent": 55.0, "dimension": "data" }
    }
  ],
  "meta": { "page": 1, "page_size": 50, "total": 318, "has_next": true }
}
```

#### Note

user_id is null for a session this instance cannot tie to a subscriber row, such as an anonymous voucher or an orphan left by a NAS restart. status and usage are absent on those too. Do not assume every live session has an owner.


### Disconnect many sessions

`POST /api/v1/admin/sessions/bulk-disconnect`

Sends a Disconnect-Request per session id and reports the per-id outcome. One request against the write budget rather than one per session.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_live_traffic` (Live traffic / disconnect)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `ids` | body | array | yes | Session ids. |

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_INTERNAL` | 503 | no CoA dispatcher is wired |

#### Note

The same NAS-accepted-it caveat as the single disconnect applies to every id in the report, and the partial-failure envelope means a 200 does not mean all of them were sent.


### Lock user MAC

`POST /api/v1/admin/sessions/bulk-lock-mac`

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_mac_lock` (Lock user MAC)
- Risk: write
- Rate limit bucket: `t_mutate`

_This endpoint has no hand-written reference entry yet. The method, path, authentication, permission and rate limit above are generated from the running router and are accurate; there is no request or response example._


### Ping user

`POST /api/v1/admin/sessions/bulk-ping`

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_ping` (Ping user)
- Risk: read
- Rate limit bucket: `t_mutate`

_This endpoint has no hand-written reference entry yet. The method, path, authentication, permission and rate limit above are generated from the running router and are accurate; there is no request or response example._


### Fetch one live session

`GET /api/v1/admin/sessions/{id}`

One session with the full projection, including the joined subscriber attributes.

- Authentication: manager session (JWT) or API token
- Permission: `prm_report_sessions` (Sessions report)
- Risk: read

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | The accounting row id, not the subscriber id. |

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_NOT_FOUND` | 404 | no such session, or one outside the caller's subtree |


### Disconnect a session

`POST /api/v1/admin/sessions/{id}/disconnect`

Sends an RFC 5176 Disconnect-Request straight from this platform to the NAS that owns the session. The packet goes out over UDP to the device, not through the RADIUS server.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_live_traffic` (Live traffic / disconnect)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Session id. |

#### Response — 200 OK

```json
{
  "data": { "ok": true, "nak": false, "message": "disconnected" }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_NOT_FOUND` | 404 | no such session, or one outside the caller's subtree |
| `ERR_INTERNAL` | 502 | the NAS did not answer, or the exchange failed. details.reason carries the transport error |
| `ERR_INTERNAL` | 503 | no CoA dispatcher is wired in this deployment |

#### Note

ok true means the NAS ACCEPTED the packet, not that the subscriber is off the network. Whether the session actually ends is up to the device, and it will be reflected in accounting a moment later. nak true is also a success from your point of view: the NAS replied "no such session", so it is already gone. Only a 502 means the disconnect did not happen.


### Read near-real-time throughput

`GET /api/v1/admin/sessions/{id}/live-traffic`

Returns bits per second when the session's NAS has SNMP management credentials with an interface selector configured, by polling the device twice about a second apart. Without them it degrades to the accounting counters.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_live_traffic` (Live traffic / disconnect)
- Risk: write

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Session id. |

#### Note

Check the source field before trusting the numbers. source "snmp" is a real measurement; source "interim" means there were no SNMP credentials, the device was unreachable, or the sampler is not wired — the counters are the last accounting values and bps is 0, and your client has to derive the rate from successive polls itself.


### Live traffic / disconnect

`GET /api/v1/admin/sessions/{id}/live-traffic/stream`

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_live_traffic` (Live traffic / disconnect)
- Risk: write

_This endpoint has no hand-written reference entry yet. The method, path, authentication, permission and rate limit above are generated from the running router and are accurate; there is no request or response example._


### Bind the subscriber to this session's device

`POST /api/v1/admin/sessions/{id}/lock-mac`

Writes the session's calling-station MAC into the subscriber's locked-MAC list. From the next authentication onward only that device may connect. The current session is left up, since it already uses the locked MAC.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_mac_lock` (Lock user MAC)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Session id. |

#### Response — 200 OK

```json
{
  "data": {
    "user_id": 4711,
    "mac": "AA:BB:CC:DD:EE:FF",
    "locked_macs": ["AA:BB:CC:DD:EE:FF"]
  }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 422 | the session has no owning subscriber, so there is no record to lock |

#### Note

This endpoint also turns the lock ON, which the subscriber PATCH does not. Writing mac_locked_addrs through PATCH /users/{id} without setting mac_lock_enabled stores a list that enforces nothing.


### Ping the subscriber's device

`POST /api/v1/admin/sessions/{id}/ping`

Sends ICMP echo to the session's framed IP from the platform. An operator diagnostic, not a monitoring feed.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_ping` (Ping user)
- Risk: read
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Session id. |

#### Note

On the per-tenant probe budget, which is 10 a minute on a standard licence. It is sized for an operator clicking a button; a polling loop will exhaust it in six seconds and take every other probe endpoint down with it.


### Poll a session's counters

`GET /api/v1/admin/sessions/{id}/traffic`

The lightweight counter snapshot behind a live traffic graph. It is a snapshot, not a rate — derive throughput from the difference between two polls and the difference between their sampled_at stamps.

- Authentication: manager session (JWT) or API token
- Permission: `prm_report_sessions` (Sessions report)
- Risk: read

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Session id. |

#### Response — 200 OK

```json
{
  "data": {
    "id": 8821904,
    "session_time": 6900,
    "input_octets": 412000000,
    "output_octets": 5100000000,
    "update_time": "2026-09-20 09:06:02",
    "sampled_at": "2026-09-20 09:08:31"
  }
}
```

#### Note

update_time is when the NAS last reported, and sampled_at is when this server read it. They differ by up to one accounting interval, so two polls closer together than that interval return identical counters and a throughput of zero. That is the NAS being quiet, not the subscriber.


