---
name: x-radius-api
description: Call the X-Radius REST API - subscribers, prepaid cards and vouchers, NAS devices, live sessions, billing, payments, managers and support tickets. Use when a task needs to read or change data in an X-Radius instance, when a user mentions X-Radius, their operator portal or a tenant host, or when a credential starting with xrt_ appears.
---

# X-Radius API

## What this is

X-Radius is a multi-tenant RADIUS billing platform for internet providers.
Every screen in its operator portal calls the same public REST API described
here. There is no second, private API.

Each customer (a tenant) is reached on its own host, so the base URL is the
tenant's, not a shared one:

```
https://<tenant-slug>.<instance-domain>/api/v1
```

Use the host the operator sees in the browser when signed in to the panel. Ask
for it rather than guessing it: the slug is per customer and the instance
domain is per deployment.

## Authenticating

One header, on every request:

```
Authorization: Bearer xrt_...
```

An `xrt_` token is a manager API token, created in the panel under
Developer, API Tokens. It belongs to exactly one staff member. Treat it as a
password: anyone holding it can act as its owner until it is revoked.

A manager session JWT works in the same header and is what the portal itself
uses, but a script should hold a token, not a session: sessions expire on a
short clock and a few routes refuse a machine credential outright.

Confirm a credential before building on it:

```
curl -s https://<tenant-host>/api/v1/auth/me \
  -H "Authorization: Bearer $XRADIUS_TOKEN"
```

## What a token is allowed to do

A token's authority is the INTERSECTION of two things, and it is
recomputed on every request:

1. what its owner may do right now, and
2. the scope the token was created with.

Both directions are live. Demote the owner and every token they hold narrows
with them, immediately and with no token edit. Widen the scope past what the
owner holds and nothing happens, because a scope is a ceiling and not a grant.
A scoped token is restricted even when its owner is an administrator, and it
reports administrator status as false for exactly that reason - an integration
must branch on the permissions it is given, never on an administrator flag.

An empty scope means the token may do nothing. It does not mean unrestricted.

Token management itself is never reachable with a token, so a token cannot
widen its own scope.

## The response envelope

A single object comes back under `data`:

```json
{ "data": { "id": 4711, "username": "ahmed" } }
```

A list adds `meta`:

```json
{
  "data": [ { "id": 4711, "username": "ahmed" } ],
  "meta": { "page": 2, "page_size": 100, "total": 812, "has_next": true }
}
```

Read `meta` back rather than assuming the request was taken verbatim.
`page` and `page_size` are echoed as the server RESOLVED them,
after clamping. Use `has_next` to decide whether to fetch another page;
deriving it from `total` and `page_size` is the classic
off-by-one at the last page.

## Listing, filtering and searching

Every list endpoint reads the same six query parameters:

| Parameter | Default | Notes |
| --- | --- | --- |
| `page` | 1 | One-indexed. |
| `page_size` | 50 | Clamped to 200. |
| `sort` | per endpoint | Unknown column falls back to that endpoint's default. |
| `order` | desc | Only asc and desc are honoured. |
| `q` | - | Free-text search; which columns it covers is per endpoint. |
| `filter[key]` | - | Structured narrowing, bracket syntax, keys are per endpoint. |

Out-of-range values are CLAMPED, not refused, so a silently wrong parameter
looks exactly like a correct one. There is no global filter registry: a filter
key an endpoint does not know is ignored silently and narrows nothing, while a
known key with an unparseable value usually returns a validation error.

Server-side scoping is applied on top of whatever is sent. A manager without
tenant-wide visibility sees only their own subtree, and no filter can widen
past it.

## Errors

Every non-2xx response is the same object, including a 500. There is no second
error shape and no HTML error page:

```json
{
  "error": {
    "code": "ERR_VALIDATION",
    "message": "Request validation failed.",
    "request_id": "8f2c1d0a4b",
    "details": { "reason": "invalid_expiration" }
  }
}
```

Branch on `code`. It is a stable, uppercase class. NEVER branch on
`message`: it is prose for a human, already translated into the
caller's language, and unstable in both wording and language. Quote
`request_id` when reporting a problem; it identifies the exact request
in the server log.

The class vocabulary is small: `ERR_VALIDATION` (400),
`ERR_UNAUTHORIZED` (401), `ERR_FORBIDDEN` (403),
`ERR_NOT_FOUND` (404), `ERR_CONFLICT` (409),
`ERR_RATE_LIMITED` (429), `ERR_INTERNAL` (500),
`ERR_TIMEOUT` (503), `ERR_LICENSE_BLOCKED` (403) and
`ERR_MAINTENANCE` (503).

Two are worth knowing before they happen. `ERR_NOT_FOUND` covers both
"does not exist" and "exists but is outside your subtree" - the two answers are
byte-identical on purpose, so ids cannot be enumerated. `ERR_TIMEOUT`
means the query was too big for the time allowed, not that anything broke:
narrow the filter or the page and retry.

When a finer distinction is needed, read `details.reason`, a stable
discriminator present on some failures. Do not try to enumerate the reasons;
switch on the few a given flow can produce.

## Timestamps, money and identifiers

- Timestamps are `yyyy-MM-dd HH:mm:ss` in UTC, on the wire in both
  directions. There is no offset suffix and no other format.
- Money is a bare JSON number in MAJOR units, for example `100.50`,
  never a string and never minor units. The currency travels beside it as an
  ISO-4217 code such as `EGP`. Two decimal places are exact.
- A subscriber is identified within a tenant, never globally: two tenants may
  each have a subscriber called `bob`, and that is normal. Nothing in
  the request body or the query string selects a tenant - the credential does.

## Retrying safely

Redeem and activate endpoints take a client-supplied `request_id`, a
UUID generated by the caller:

```json
{ "request_id": "3f8b0c6e-1a24-4d9a-9b0f-71d3b0d9c5a7", "code": "ABCD-1234" }
```

Retrying with the SAME id returns the original result rather than acting twice.
Generate the id once per logical operation and reuse it across retries.
Generating a fresh one on retry is what double-redeems a card. An endpoint that
takes one is marked in the reference.

Everything else follows ordinary HTTP semantics: GET and DELETE are safe to
retry, POST and PATCH are not unless the endpoint documents an idempotency key.

## Rate limits

Budgets are per-minute counters in a fixed window, shared across API replicas
and scaled by the tenant's licence tier. Every non-GET on the authenticated
surface is counted once; expensive reads, operator-triggered network probes and
fan-out sends are counted again by their own bucket, so one heavy write can
consume two budgets. Each endpoint page names the bucket it draws from.

A 429 carries no `Retry-After` header and no remaining-budget header.
The window is one minute, so backing off from a few seconds and retrying is
correct.

## Finding the right endpoint

Work from the machine feeds rather than crawling the HTML:

| Need | Fetch |
| --- | --- |
| Find an endpoint by name or path | `https://x-radius.com/llms.txt` |
| Everything, in one file | `https://x-radius.com/llms-full.txt` |
| Generate a client | `https://x-radius.com/openapi.json` |
| One endpoint in full | append .md to its page URL |
| Any reference page as Markdown | send `Accept: text/markdown` |

Start with `/llms.txt`. It is one line per endpoint, so it is small
enough to read whole, and each line links to the page whose `.md` twin
carries the detail. Fetch the full file only when the index is genuinely not
enough.

Note the two hosts. The reference is served from the instance's public site,
while the API answers on the TENANT host. The feeds above are reachable without
a credential; the API is not.

## What this reference does and does not carry

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

A generated entry is still correct and still worth reading: its method, path, authentication class, required permission and rate-limit bucket are read from the router that serves the instance, not written by hand. What it does not have is a worked request or response body. Do NOT invent one. When an endpoint carries no example, say so, and derive the shape from a documented endpoint in the same resource group or from the operator rather than presenting a guess as the contract.

Response schemas are absent from the OpenAPI document for the same reason: roughly a sixth of the handlers write an inline structure with no named type, so a document carrying schemas for most endpoints and silently wrong ones for the rest would be worse than one that carries examples where they were written.

## Resource groups

| Group | Endpoints | Written | Index |
| --- | --- | --- | --- |
| Users | 92 | 28 | https://x-radius.com/docs/api/users |
| Tickets | 83 | 0 | https://x-radius.com/docs/api/tickets |
| NAS | 69 | 12 | https://x-radius.com/docs/api/nas |
| Portal | 58 | 0 | https://x-radius.com/docs/api/portal |
| User | 58 | 0 | https://x-radius.com/docs/api/user |
| Tools | 48 | 0 | https://x-radius.com/docs/api/tools |
| Monitoring | 45 | 0 | https://x-radius.com/docs/api/monitoring |
| Managers | 32 | 14 | https://x-radius.com/docs/api/managers |
| Public | 29 | 0 | https://x-radius.com/docs/api/public |
| Card Batches | 28 | 12 | https://x-radius.com/docs/api/card-batches |
| Profiles | 26 | 13 | https://x-radius.com/docs/api/profiles |
| Auth | 24 | 5 | https://x-radius.com/docs/api/auth |
| Reports | 18 | 0 | https://x-radius.com/docs/api/reports |
| Studio | 18 | 0 | https://x-radius.com/docs/api/studio |
| Payments | 14 | 7 | https://x-radius.com/docs/api/payments |
| Freezone | 12 | 0 | https://x-radius.com/docs/api/freezone |
| POS | 11 | 0 | https://x-radius.com/docs/api/pos |
| Sessions | 11 | 8 | https://x-radius.com/docs/api/sessions |
| Cards | 10 | 5 | https://x-radius.com/docs/api/cards |
| Developer | 10 | 0 | https://x-radius.com/docs/api/developer |
| Devgw | 10 | 0 | https://x-radius.com/docs/api/devgw |
| Hotspot | 10 | 0 | https://x-radius.com/docs/api/hotspot |
| Card Templates | 9 | 0 | https://x-radius.com/docs/api/card-templates |
| IP Pools | 9 | 0 | https://x-radius.com/docs/api/ip-pools |
| Shop | 9 | 0 | https://x-radius.com/docs/api/shop |
| Webhooks | 9 | 8 | https://x-radius.com/docs/api/webhooks |
| Addons | 8 | 0 | https://x-radius.com/docs/api/addons |
| Dashboards | 8 | 0 | https://x-radius.com/docs/api/dashboards |
| Forms | 8 | 0 | https://x-radius.com/docs/api/forms |
| Sas4 | 8 | 0 | https://x-radius.com/docs/api/sas4 |
| Backup | 7 | 0 | https://x-radius.com/docs/api/backup |
| Control | 7 | 0 | https://x-radius.com/docs/api/control |
| Exports | 7 | 6 | https://x-radius.com/docs/api/exports |
| Notification Outbox | 7 | 0 | https://x-radius.com/docs/api/notification-outbox |
| Notification Rules | 7 | 0 | https://x-radius.com/docs/api/notification-rules |
| Roles | 7 | 6 | https://x-radius.com/docs/api/roles |
| Branding | 6 | 0 | https://x-radius.com/docs/api/branding |
| Email Templates | 6 | 0 | https://x-radius.com/docs/api/email-templates |
| Import | 6 | 0 | https://x-radius.com/docs/api/import |
| Notification Channels | 6 | 0 | https://x-radius.com/docs/api/notification-channels |
| Trash | 6 | 0 | https://x-radius.com/docs/api/trash |
| Groups | 5 | 0 | https://x-radius.com/docs/api/groups |
| Telegram | 5 | 0 | https://x-radius.com/docs/api/telegram |
| Console Sessions | 4 | 0 | https://x-radius.com/docs/api/console-sessions |
| Notification Templates | 4 | 0 | https://x-radius.com/docs/api/notification-templates |
| Pricing | 4 | 0 | https://x-radius.com/docs/api/pricing |
| Settings | 4 | 0 | https://x-radius.com/docs/api/settings |
| Setup | 4 | 0 | https://x-radius.com/docs/api/setup |
| Telegram Manager | 4 | 0 | https://x-radius.com/docs/api/telegram-manager |
| Billing | 3 | 2 | https://x-radius.com/docs/api/billing |
| Logs | 2 | 0 | https://x-radius.com/docs/api/logs |
| License | 1 | 0 | https://x-radius.com/docs/api/license |
| Notification Event Options | 1 | 0 | https://x-radius.com/docs/api/notification-event-options |
| Notification Events | 1 | 0 | https://x-radius.com/docs/api/notification-events |
| Permissions | 1 | 1 | https://x-radius.com/docs/api/permissions |
| Postpaid | 1 | 0 | https://x-radius.com/docs/api/postpaid |
| System | 1 | 0 | https://x-radius.com/docs/api/system |
| Tenants | 1 | 0 | https://x-radius.com/docs/api/tenants |

