# 3. API reference

Base URL: `https://<host>/api`. Interactive documentation (OpenAPI/Swagger) is served at `/api/docs` by the running API. This document covers the conventions and the endpoint map; request/response schemas are the zod schemas in `packages/shared/src/dto/*.ts`.

## 3.1 Authentication

| Caller | Mechanism | Notes |
|--------|-----------|-------|
| Back office / RF users | `POST /api/auth/login` with `tenantCode`, `email`, `password` (+ optional `deviceId`) returns `tokens.accessToken` (JWT, 15 min) and `tokens.refreshToken` (rotating, also set as an httpOnly cookie). Send `Authorization: Bearer <accessToken>`. | `POST /api/auth/refresh` rotates the pair; a reused refresh token is rejected. `POST /api/auth/logout` revokes the session. |
| Platform operators | `POST /api/auth/platform/login` returns a short-lived access token with RLS bypass. | Only `/api/platform/*` and read endpoints. |
| Integrations (ERP, storer systems) | `X-API-Key: <key>` created in Integrations > API keys. Keys are scoped (`items:read`, `items:write`, `asn:read`, `asn:write`, `orders:read`, `orders:write`, `inventory:read`, `serials:read`, `billing:read`, `webhooks:manage`) and may be bound to one storer. | Keys only reach endpoints that declare `@ApiKeyScopes`: the public API `/api/v1/*` plus a few read endpoints of the back-office API (`/inbound/asns`, `/inventory/balances|movements|serials`, `/outbound/orders`, `/billing/charges|storage/report|invoices`). Every call is metered against the plan's daily quota (`429 PLAN_LIMIT`). |

The JWT carries the tenant, permissions (with wildcards such as `inbound.*`), accessible warehouse ids, storer scope (portal users) and enabled feature keys, so authorisation needs no database round trip.

## 3.2 Conventions

- JSON everywhere except ERP document ingestion (`application/xml`, `text/csv`) and uploads (`multipart/form-data`).
- Quantities, money and rates are decimal **strings** in requests and responses (`"12"`, `"1.2500"`). Dates are `YYYY-MM-DD`; timestamps are ISO 8601.
- Lists are paginated: `?page=1&pageSize=50&q=text` and return `{ items, total, page, pageSize }` (a `sort` parameter is accepted for forward compatibility but lists have fixed, documented orderings).
- Errors: `{ statusCode, error, code, message, details?, requestId }`. Stable `code` values include `VALIDATION_ERROR` (400), `FEATURE_DISABLED` / `WAREHOUSE_ACCESS` / `STORER_SCOPE` (403), `NOT_FOUND` (404), `DUPLICATE` (409), `INVALID_STATE`, `INSUFFICIENT_STOCK`, `LOT_REQUIRED`, `EXPIRY_REQUIRED`, `SERIAL_REQUIRED`, `OVER_RECEIPT`, `WRONG_LOCATION`, `WRONG_LPN`, `WRONG_SKU`, `LOCATION_CAPACITY`, `CROSS_TENANT_REFERENCE` (422), `PLAN_LIMIT` (422/429), `AI_DISABLED` (503).
- Idempotency: `POST /api/v1/asns`, `POST /api/v1/orders`, `PUT /api/v1/items` and ERP ingestion honour an `Idempotency-Key` header; receiving and task confirmations accept `idempotencyKey` in the body; charge capture is idempotent per tariff and document.
- Warehouse scope: most list endpoints take `warehouseId`; users only see warehouses assigned to them. Storer-scoped users (portal) are automatically restricted to their storer.
- The public API (`/api/v1`) uses business codes (`warehouseCode`, `itemCode`, `reference`), never internal UUIDs.

## 3.3 Public API v1 quick reference

```http
PUT  /api/v1/items                 { "items": [{ "code": "SKU-1", "description": "...", "barcodes": [{ "barcode": "629..." }], "packs": [{ "level": "CASE", "uomCode": "CS", "qtyInBase": 6 }] }] }
POST /api/v1/asns                  { "warehouseCode": "KW1", "reference": "PO-2001", "lines": [{ "itemCode": "SKU-1", "qty": 10, "packLevel": "CASE", "lot": "L1", "expiryDate": "2027-06-30" }] }   (Idempotency-Key header)
GET  /api/v1/asns/PO-2001
POST /api/v1/orders                { "warehouseCode": "KW1", "reference": "SO-9001", "shipTo": { "name": "Store Z" }, "lines": [{ "itemCode": "SKU-1", "qty": 2 }] }
POST /api/v1/orders/SO-9001/cancel
GET  /api/v1/inventory?groupBy=ITEM|LOT|LPN
GET  /api/v1/serials?itemCode=SKU-1
POST /api/v1/erp/{connectorId}/inbound   (raw IDoc XML / CSV body; scopes asn:write + orders:write; 202 Accepted with per-document results and a message log)
GET  /api/v1/erp/outbox                   (outbound ERP documents waiting for pickup) ; POST /api/v1/erp/outbox/ack
```

Webhooks: subscriptions receive `POST` with JSON `{ id, type, occurredAt, tenantId, warehouseId, storerId, payload }` and headers `X-WMS-Event`, `X-WMS-Delivery`, `X-WMS-Signature: sha256=<hex HMAC of the raw body with the subscription secret>`. Non-2xx responses are retried with exponential backoff and parked as DEAD after the maximum attempts; redelivery is available from the UI. Event catalogue: `packages/shared/src/dto/integrations.ts` (`WEBHOOK_EVENTS`).

## 3.4 RF (handheld) flow endpoints

| Step | Endpoint |
|------|----------|
| Get next task of a type (assigns it) | `POST /api/tasks/next` `{ warehouseId, types: ["PICK"], zoneId? }` |
| Start / confirm / exception | `POST /api/tasks/:id/start`, `POST /api/tasks/:id/confirm` (scan confirmations: `fromLocationCode`, `toLocationCode`, `lpnNumber`, `toLpnNumber`, `barcode`, `qty`, `packLevel`, `shortQty`, `serials`, `lotNumber`, `expiryDate`), `POST /api/tasks/:id/exception` `{ code, notes }` |
| Receive a line | `POST /api/inbound/asns/:id/receive` (`asnLineId` or `barcode`, `qty`, `packLevel`, `lotNumber`, `expiryDate`, `condition`, `lpnNumber`, `serials`, `idempotencyKey`) |
| Ad hoc move | `POST /api/inventory/moves` |
| Count entry | `GET /api/inventory/counts/:id/locations/:code` then `POST /api/inventory/counts/:id/submit` |
| Inquiry | `GET /api/inventory/lpns/:lpnNumber`, `GET /api/inventory/locations/:warehouseId/:code`, `GET /api/skus?q=` |

## 3.5 Endpoint map (235 routes)

| Module | Method | Path | Requires |
|--------|--------|------|----------|
| ai | `GET` | `/api/ai/status` | ai.use |
| ai | `POST` | `/api/ai/intake` | ai.use, feature ai.document_intake |
| ai | `POST` | `/api/ai/intake/upload` | ai.use, feature ai.document_intake |
| ai | `POST` | `/api/ai/discrepancy-email` | ai.use, feature ai.document_intake |
| ai | `POST` | `/api/ai/analyses/run` | ai.use |
| ai | `POST` | `/api/ai/copilot/ask` | ai.use, feature ai.nl_query |
| ai | `GET` | `/api/ai/copilot/conversations` | ai.use |
| ai | `GET` | `/api/ai/copilot/conversations/:id` | ai.use |
| ai | `GET` | `/api/ai/proposals` | ai.use |
| ai | `GET` | `/api/ai/proposals/inbox` | ai.use |
| ai | `GET` | `/api/ai/proposals/:id` | ai.use |
| ai | `POST` | `/api/ai/proposals/:id/decide` | ai.decide |
| auth | `POST` | `/api/auth/login` |  |
| auth | `POST` | `/api/auth/platform/login` | public |
| auth | `POST` | `/api/auth/refresh` |  |
| auth | `POST` | `/api/auth/logout` |  |
| auth | `GET` | `/api/auth/me` |  |
| auth | `POST` | `/api/auth/change-password` |  |
| billing | `GET` | `/api/billing/contracts` | billing.read |
| billing | `GET` | `/api/billing/contracts/:id` | billing.read |
| billing | `POST` | `/api/billing/contracts` | billing.manage |
| billing | `PATCH` | `/api/billing/contracts/:id` | billing.manage |
| billing | `POST` | `/api/billing/contracts/:id/tariffs` | billing.manage |
| billing | `PATCH` | `/api/billing/tariffs/:id` | billing.manage |
| billing | `DELETE` | `/api/billing/tariffs/:id` | billing.manage |
| billing | `GET` | `/api/billing/charges` | billing.read, api key: billing:read |
| billing | `POST` | `/api/billing/charges` | billing.manage |
| billing | `POST` | `/api/billing/charges/:id/waive` | billing.manage |
| billing | `GET` | `/api/billing/storage/report` | billing.read, api key: billing:read |
| billing | `POST` | `/api/billing/storage/snapshot` | billing.manage |
| billing | `GET` | `/api/billing/invoices` | billing.read, api key: billing:read |
| billing | `GET` | `/api/billing/invoices/:id` | billing.read, api key: billing:read |
| billing | `POST` | `/api/billing/invoices/run` | billing.invoice |
| billing | `POST` | `/api/billing/invoices/:id/issue` | billing.invoice |
| billing | `POST` | `/api/billing/invoices/:id/cancel` | billing.invoice |
| billing | `POST` | `/api/billing/invoices/:id/payments` | billing.invoice |
| billing | `POST` | `/api/billing/invoices/:id/credit-notes` | billing.invoice |
| billing | `POST` | `/api/billing/invoices/:id/dispute` | billing.manage |
| documents | `GET` | `/api/documents` | reports.read |
| documents | `GET` | `/api/documents/:id/download` | reports.read |
| documents | `GET` | `/api/documents/labels/lpn/:lpnNumber` | inventory.read |
| documents | `GET` | `/api/documents/labels/carton/:cartonId` | outbound.read |
| documents | `GET` | `/api/documents/labels/location/:code` | masterdata.read |
| inbound | `GET` | `/api/inbound/asns` | inbound.read, api key: asn:read |
| inbound | `GET` | `/api/inbound/asns/:id` | inbound.read, api key: asn:read |
| inbound | `POST` | `/api/inbound/asns` | inbound.manage |
| inbound | `PATCH` | `/api/inbound/asns/:id` | inbound.manage |
| inbound | `POST` | `/api/inbound/asns/:id/arrive` | inbound.receive |
| inbound | `POST` | `/api/inbound/asns/:id/cancel` | inbound.manage |
| inbound | `POST` | `/api/inbound/asns/:id/receive` | inbound.receive |
| inbound | `POST` | `/api/inbound/receipts/:id/reverse` | inbound.receive |
| inbound | `POST` | `/api/inbound/asns/:id/received` | inbound.receive |
| inbound | `POST` | `/api/inbound/asns/:id/close` | inbound.close |
| inbound | `PATCH` | `/api/inbound/discrepancies/:id` | inbound.close |
| inbound | `GET` | `/api/inbound/dock-appointments` | inbound.read |
| inbound | `POST` | `/api/inbound/dock-appointments` | inbound.manage |
| inbound | `PATCH` | `/api/inbound/dock-appointments/:id` | inbound.manage |
| integrations | `PUT` | `/api/v1/items` | masterdata.manage, api key: items:write |
| integrations | `GET` | `/api/v1/items` | masterdata.read, api key: items:read |
| integrations | `POST` | `/api/v1/asns` | inbound.manage, api key: asn:write |
| integrations | `GET` | `/api/v1/asns/:reference` | inbound.read, api key: asn:read |
| integrations | `POST` | `/api/v1/orders` | outbound.manage, api key: orders:write |
| integrations | `GET` | `/api/v1/orders/:reference` | outbound.read, api key: orders:read |
| integrations | `POST` | `/api/v1/orders/:reference/cancel` | outbound.manage, api key: orders:write |
| integrations | `GET` | `/api/v1/inventory` | inventory.read, api key: inventory:read |
| integrations | `GET` | `/api/v1/serials` | inventory.read, api key: serials:read |
| integrations | `POST` | `/api/v1/erp/:connectorId/inbound` | inbound.manage |
| integrations | `GET` | `/api/v1/erp/outbox` | inbound.read |
| integrations | `POST` | `/api/v1/erp/outbox/ack` | inbound.read |
| integrations | `GET` | `/api/integrations/webhooks` | integrations.read |
| integrations | `POST` | `/api/integrations/webhooks` | integrations.manage, feature integrations.webhooks |
| integrations | `PATCH` | `/api/integrations/webhooks/:id` | integrations.manage |
| integrations | `DELETE` | `/api/integrations/webhooks/:id` | integrations.manage |
| integrations | `GET` | `/api/integrations/webhooks/:id/deliveries` | integrations.read |
| integrations | `POST` | `/api/integrations/deliveries/:id/redeliver` | integrations.manage |
| integrations | `GET` | `/api/integrations/connectors` | integrations.read |
| integrations | `POST` | `/api/integrations/connectors` | integrations.manage |
| integrations | `PATCH` | `/api/integrations/connectors/:id` | integrations.manage |
| integrations | `POST` | `/api/integrations/connectors/:id/inbound` | integrations.manage |
| integrations | `GET` | `/api/integrations/messages` | integrations.read |
| integrations | `GET` | `/api/integrations/messages/:id` | integrations.read |
| integrations | `POST` | `/api/integrations/messages/:id/retry` | integrations.manage |
| integrations | `GET` | `/api/integrations/imports` | integrations.read |
| integrations | `GET` | `/api/integrations/imports/:id` | integrations.read |
| integrations | `GET` | `/api/integrations/imports/template/:type` | integrations.read |
| integrations | `POST` | `/api/integrations/imports` | integrations.manage |
| inventory | `GET` | `/api/inventory/balances` | inventory.read, api key: inventory:read |
| inventory | `GET` | `/api/inventory/movements` | inventory.read, api key: inventory:read |
| inventory | `GET` | `/api/inventory/serials` | inventory.read, api key: serials:read |
| inventory | `GET` | `/api/inventory/lpns/:lpnNumber` | inventory.read |
| inventory | `GET` | `/api/inventory/locations/:warehouseId/:code` | inventory.read |
| inventory | `POST` | `/api/inventory/lpns` | inventory.move |
| inventory | `POST` | `/api/inventory/moves` | inventory.move |
| inventory | `GET` | `/api/inventory/holds` | inventory.read |
| inventory | `POST` | `/api/inventory/holds` | inventory.hold |
| inventory | `POST` | `/api/inventory/holds/:id/release` | inventory.hold |
| inventory | `GET` | `/api/inventory/adjustments` | inventory.read |
| inventory | `GET` | `/api/inventory/adjustments/:id` | inventory.read |
| inventory | `POST` | `/api/inventory/adjustments` | inventory.adjust.request |
| inventory | `POST` | `/api/inventory/adjustments/:id/submit` | inventory.adjust.request |
| inventory | `POST` | `/api/inventory/adjustments/:id/decide` | inventory.adjust.approve |
| inventory | `POST` | `/api/inventory/adjustments/:id/cancel` | inventory.adjust.request |
| inventory | `GET` | `/api/inventory/counts` | inventory.read |
| inventory | `GET` | `/api/inventory/counts/:id` | inventory.read |
| inventory | `POST` | `/api/inventory/counts` | inventory.count.manage |
| inventory | `POST` | `/api/inventory/counts/:id/release` | inventory.count.manage |
| inventory | `GET` | `/api/inventory/counts/:id/locations/:code` | inventory.count.perform |
| inventory | `POST` | `/api/inventory/counts/:id/submit` | inventory.count.perform |
| inventory | `POST` | `/api/inventory/counts/:id/review` | inventory.count.manage |
| inventory | `POST` | `/api/inventory/counts/:id/cancel` | inventory.count.manage |
| master-data | `GET` | `/api/warehouses` | masterdata.read |
| master-data | `GET` | `/api/warehouses/:id` | masterdata.read |
| master-data | `POST` | `/api/warehouses` | masterdata.manage |
| master-data | `PATCH` | `/api/warehouses/:id` | masterdata.manage |
| master-data | `GET` | `/api/warehouses/:id/zones` | masterdata.read |
| master-data | `POST` | `/api/zones` | masterdata.manage |
| master-data | `PATCH` | `/api/zones/:id` | masterdata.manage |
| master-data | `GET` | `/api/warehouses/:id/locations` | masterdata.read |
| master-data | `POST` | `/api/locations` | masterdata.manage |
| master-data | `POST` | `/api/locations/generate` | masterdata.manage |
| master-data | `PATCH` | `/api/locations/:id` | masterdata.manage |
| master-data | `GET` | `/api/storers` | masterdata.read |
| master-data | `GET` | `/api/storers/:id` | masterdata.read |
| master-data | `POST` | `/api/storers` | masterdata.manage |
| master-data | `PATCH` | `/api/storers/:id` | masterdata.manage |
| master-data | `GET` | `/api/suppliers` | masterdata.read |
| master-data | `POST` | `/api/suppliers` | masterdata.manage |
| master-data | `PATCH` | `/api/suppliers/:id` | masterdata.manage |
| master-data | `GET` | `/api/customers` | masterdata.read |
| master-data | `POST` | `/api/customers` | masterdata.manage |
| master-data | `PATCH` | `/api/customers/:id` | masterdata.manage |
| master-data | `GET` | `/api/carriers` | masterdata.read |
| master-data | `POST` | `/api/carriers` | masterdata.manage |
| master-data | `PATCH` | `/api/carriers/:id` | masterdata.manage |
| master-data | `GET` | `/api/skus` | masterdata.read |
| master-data | `GET` | `/api/skus/:id` | masterdata.read |
| master-data | `POST` | `/api/skus` | masterdata.manage |
| master-data | `PATCH` | `/api/skus/:id` | masterdata.manage |
| master-data | `PUT` | `/api/slottings` | rules.manage |
| master-data | `DELETE` | `/api/slottings/:id` | rules.manage |
| master-data | `GET` | `/api/warehouses/:id/putaway-rules` | masterdata.read |
| master-data | `POST` | `/api/putaway-rules` | rules.manage |
| master-data | `PATCH` | `/api/putaway-rules/:id` | rules.manage |
| master-data | `DELETE` | `/api/putaway-rules/:id` | rules.manage |
| master-data | `GET` | `/api/allocation-rules` | masterdata.read |
| master-data | `POST` | `/api/allocation-rules` | rules.manage |
| master-data | `PATCH` | `/api/allocation-rules/:id` | rules.manage |
| master-data | `DELETE` | `/api/allocation-rules/:id` | rules.manage |
| master-data | `GET` | `/api/reason-codes` | masterdata.read |
| master-data | `POST` | `/api/reason-codes` | masterdata.manage |
| master-data | `PATCH` | `/api/reason-codes/:id` | masterdata.manage |
| outbound | `GET` | `/api/outbound/orders` | outbound.read, api key: orders:read |
| outbound | `GET` | `/api/outbound/orders/:id` | outbound.read, api key: orders:read |
| outbound | `POST` | `/api/outbound/orders` | outbound.manage |
| outbound | `PATCH` | `/api/outbound/orders/:id` | outbound.manage |
| outbound | `POST` | `/api/outbound/orders/:id/hold` | outbound.manage |
| outbound | `POST` | `/api/outbound/orders/:id/release-hold` | outbound.manage |
| outbound | `POST` | `/api/outbound/orders/:id/cancel` | outbound.manage |
| outbound | `POST` | `/api/outbound/orders/:id/ship` | outbound.ship |
| outbound | `POST` | `/api/outbound/orders/allocate` | outbound.allocate |
| outbound | `POST` | `/api/outbound/orders/:id/accept-short` | outbound.manage |
| outbound | `POST` | `/api/outbound/orders/:id/deallocate` | outbound.allocate |
| outbound | `GET` | `/api/outbound/atp` | outbound.read |
| outbound | `GET` | `/api/outbound/waves` | outbound.read |
| outbound | `GET` | `/api/outbound/waves/:id` | outbound.read |
| outbound | `POST` | `/api/outbound/waves` | outbound.allocate |
| outbound | `POST` | `/api/outbound/waves/:id/allocate` | outbound.allocate |
| outbound | `POST` | `/api/outbound/waves/:id/release` | outbound.allocate |
| outbound | `POST` | `/api/outbound/waves/:id/pick-list` | outbound.read |
| outbound | `POST` | `/api/outbound/waves/:id/cancel` | outbound.allocate |
| outbound | `POST` | `/api/outbound/packing/start` | outbound.pack |
| outbound | `POST` | `/api/outbound/packing/carton` | outbound.pack |
| outbound | `DELETE` | `/api/outbound/packing/carton/:id` | outbound.pack |
| outbound | `POST` | `/api/outbound/packing/complete` | outbound.pack |
| outbound | `GET` | `/api/outbound/shipments/:id` | outbound.read |
| outbound | `GET` | `/api/outbound/loads` | outbound.read |
| outbound | `GET` | `/api/outbound/loads/:id` | outbound.read |
| outbound | `POST` | `/api/outbound/loads` | outbound.ship |
| outbound | `PATCH` | `/api/outbound/loads/:id` | outbound.ship |
| outbound | `POST` | `/api/outbound/loads/:id/shipments` | outbound.ship |
| outbound | `DELETE` | `/api/outbound/loads/:id/shipments/:shipmentId` | outbound.ship |
| outbound | `POST` | `/api/outbound/loads/:id/shipments/:shipmentId/loaded` | outbound.ship |
| outbound | `POST` | `/api/outbound/loads/:id/dispatch` | outbound.ship |
| outbound | `POST` | `/api/outbound/loads/:id/pod` | outbound.ship |
| outbound | `GET` | `/api/outbound/returns` | outbound.read |
| outbound | `GET` | `/api/outbound/returns/:id` | outbound.read |
| outbound | `POST` | `/api/outbound/returns` | returns.manage |
| outbound | `POST` | `/api/outbound/returns/:id/authorise` | returns.manage |
| outbound | `POST` | `/api/outbound/returns/:id/disposition` | returns.manage |
| outbound | `POST` | `/api/outbound/returns/:id/cancel` | returns.manage |
| outbound | `POST` | `/api/outbound/replenishment/run` | tasks.manage |
| platform | `GET` | `/api/platform/tenants/:id/export` | platform admin |
| platform | `GET` | `/api/platform/plans` | platform admin |
| platform | `POST` | `/api/platform/plans` | platform admin |
| platform | `PATCH` | `/api/platform/plans/:id` | platform admin |
| platform | `GET` | `/api/platform/tenants` | platform admin |
| platform | `POST` | `/api/platform/tenants` | platform admin |
| platform | `GET` | `/api/platform/tenants/:id` | platform admin |
| platform | `PATCH` | `/api/platform/tenants/:id` | platform admin |
| platform | `PATCH` | `/api/platform/tenants/:id/subscription` | platform admin |
| platform | `PUT` | `/api/platform/tenants/:id/features` | platform admin |
| platform | `GET` | `/api/platform/usage` | platform admin |
| reports | `GET` | `/api/reports/dashboard/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/occupancy/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/stock-aging/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/expiry/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/movements/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/productivity/:warehouseId` | reports.read |
| reports | `GET` | `/api/reports/export/:report/:warehouseId` | reports.export |
| tasks | `GET` | `/api/tasks` | tasks.read |
| tasks | `GET` | `/api/tasks/summary` | tasks.read |
| tasks | `GET` | `/api/tasks/:id` | tasks.read |
| tasks | `POST` | `/api/tasks/next` | tasks.execute |
| tasks | `POST` | `/api/tasks/:id/start` | tasks.execute |
| tasks | `POST` | `/api/tasks/:id/confirm` | tasks.execute |
| tasks | `POST` | `/api/tasks/:id/exception` | tasks.execute |
| tasks | `POST` | `/api/tasks/assign` | tasks.manage |
| tasks | `POST` | `/api/tasks/reprioritise` | tasks.manage |
| tasks | `POST` | `/api/tasks/cancel` | tasks.manage |
| tasks | `POST` | `/api/tasks/move` | tasks.manage |
| audit | `GET` | `/api/audit` | audit.read |
| users | `GET` | `/api/users` | users.read |
| users | `GET` | `/api/users/:id` | users.read |
| users | `POST` | `/api/users` | users.manage |
| users | `PATCH` | `/api/users/:id` | users.manage |
| users | `GET` | `/api/roles` | users.read |
| users | `GET` | `/api/roles/permissions` | users.read |
| users | `POST` | `/api/roles` | roles.manage |
| users | `PATCH` | `/api/roles/:id` | roles.manage |
| users | `DELETE` | `/api/roles/:id` | roles.manage |
| users | `GET` | `/api/api-keys` | apikeys.manage |
| users | `POST` | `/api/api-keys` | apikeys.manage |
| users | `DELETE` | `/api/api-keys/:id` | apikeys.manage |
| health | `GET` | `/api/health` |  |
| health | `GET` | `/api/health/ready` |  |
