# Billing - 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/billing
> Every group: https://x-radius.com/llms.txt

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

### List invoices across subscribers

`GET /api/v1/billing/invoices`

Every invoice the caller may see, in one list, with filters for reconciling a period. A caller without tenant-wide visibility sees only invoices belonging to subscribers in their own manager subtree.

- Authentication: manager session (JWT) or API token
- Permission: `prm_billing` (Billing & invoices)
- Risk: write

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `filter[status]` | query | string | no | Payment status. |
| `filter[username]` | query | string | no | Narrow to one subscriber by username. |
| `filter[date_from]` | query | string | no | Inclusive lower bound. A malformed value is a 400, not an ignored filter. |
| `filter[date_to]` | query | string | no | Inclusive upper bound. |
| `filter[amount_min]` | query | number | no | Inclusive lower amount bound. |
| `filter[amount_max]` | query | number | no | Inclusive upper amount bound. |

#### Response — 200 OK

```json
{
  "data": [
    {
      "id": 51204,
      "user_id": 4711,
      "username": "ahmed",
      "invoice_number": "INV-2026-000512",
      "type": "activation",
      "amount": 171.00,
      "discount": 0.00,
      "vat": 14.00,
      "status": "paid",
      "description": "Home 20M (activate)",
      "payment_method": "manager_balance",
      "due_date": "2026-09-20 00:00:00",
      "paid_on": "2026-09-20 09:15:02",
      "created_by_manager_id": 41,
      "created_by_name": "cairo-reseller",
      "created_at": "2026-09-20 09:15:02"
    }
  ],
  "meta": { "page": 1, "page_size": 50, "total": 1284, "has_next": true }
}
```

#### Note

username is populated only on this cross-subscriber list, which joins the subscriber table. The per-subscriber list and the single-invoice fetch leave it empty, because there the subscriber is already known.


### Issue an invoice

`POST /api/v1/billing/invoices`

Creates an invoice against a named subscriber from line items. discount and vat are percentages from 0 to 100, and the total is computed from the items — never taken from the request.

- Authentication: manager session (JWT) or API token
- Permission: `prm_billing` (Billing & invoices)
- Risk: write
- Rate limit bucket: `t_mutate`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `user_id` | body | integer | yes | The subscriber being invoiced. |
| `items` | body | array | yes | At least one line, each with name, qty, unit_price and an optional per-line tax. |
| `discount` | body | number | no | Percentage from 0 to 100, not an amount. |
| `vat` | body | number | no | Percentage from 0 to 100, not an amount. |
| `due_date` | body | string | no | yyyy-MM-dd HH:mm:ss in UTC. |
| `mark_paid` | body | boolean | no | Record the new invoice as already settled. |

#### Request

```json
{
  "user_id": 4711,
  "description": "September installation",
  "discount": 0,
  "vat": 14,
  "due_date": "2026-10-01 00:00:00",
  "items": [
    { "name": "Router CPE", "qty": 1, "unit_price": 900.00, "tax": 0 },
    { "name": "Installation", "qty": 1, "unit_price": 250.00, "tax": 0 }
  ]
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | user_id missing, no items, an unnamed item, or a discount or VAT outside 0 to 100 |
| `ERR_NOT_FOUND` | 404 | the subscriber does not exist or is outside the caller's subtree |

#### Note

discount and vat are PERCENTAGES, and the validator rejects anything above 100. Sending 171.00 in the vat field to mean "171 in tax" is a 400, not a silently large invoice.


### Billing & invoices

`POST /api/v1/billing/invoices/bulk-pay`

- Authentication: manager session (JWT) or API token
- Permission: `prm_billing` (Billing & invoices)
- 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._


