### List subscribers

`GET /api/v1/users`

One page of subscribers under the caller's authority. A manager without tenant-wide visibility sees only subscribers owned by their own manager subtree, and that scope is injected server-side, so a filter can narrow within it but never widen past it.

- Authentication: manager session (JWT) or API token
- Permission: `prm_users_index` (View users)
- Risk: read
- Rate limit bucket: `user_list`

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `q` | query | string | no | Searches username, email, first and last name, phone, contract id, national id and static IP. A purely numeric term also matches the subscriber id exactly. |
| `filter[enabled]` | query | boolean | no | The admin suspend flag. Not the same thing as expired. |
| `filter[expired]` | query | boolean | no | Subscription window has passed. |
| `filter[online]` | query | boolean | no | An open RADIUS session exists right now. |
| `filter[profile_id]` | query | integer | no | Bound plan. |
| `filter[parent_id]` | query | integer | no | Owning manager. Ignored, not honoured, when it would widen a restricted caller's scope. |
| `filter[username_exact]` | query | string | no | Case-insensitive exact username, for a caller that already knows the identity and wants one deterministic row instead of a page of substring hits. |
| `filter[expiring_within_days]` | query | integer | no | Future expiries only. |
| `filter[created_from]` | query | string | no | yyyy-MM-dd HH:mm:ss in UTC. A malformed value is a 400, not an ignored filter. |

#### Response — 200 OK

```json
{
  "data": [
    {
      "id": 4711,
      "tenant_id": 12,
      "username": "ahmed",
      "email": "ahmed@example.test",
      "enabled": true,
      "status": {
        "enabled": true,
        "deleted": false,
        "online": true,
        "expired": false,
        "have_quota": true,
        "fup": false,
        "can_connect": true
      },
      "profile_id": 7,
      "profile_name": "Home 20M",
      "parent_id": 41,
      "parent_username": "cairo-reseller",
      "bytes_remaining": 48318382080,
      "plan_bytes_limit": 107374182400,
      "used_bytes": 59055800320,
      "expiration": "2026-10-14 00:00:00",
      "balance": 0.00,
      "debt": 0.00,
      "mac_lock_enabled": false,
      "mac_locked_addrs": [],
      "enforces_bytes": true,
      "enforces_seconds": false,
      "created_at": "2026-03-02 11:40:12"
    }
  ],
  "meta": { "page": 1, "page_size": 50, "total": 812, "has_next": true }
}
```

#### Errors

| Code | Status | When |
| --- | --- | --- |
| `ERR_VALIDATION` | 400 | filter[created_from] is not a parseable timestamp |

#### Note

The per-subscriber scan token (lookup_token) is deliberately absent from list rows. It is served by GET /users/{id} and GET /users/resolve only, because a page of a thousand rows is a bulk harvest of codes that each unlock a subscriber record.

