Skip to content

Public API (v3) ​

Remnawave Admin exposes a read/write HTTP API for external integrations at /api/v3/*. Authentication is by API key. Permissions are controlled by scopes.

  • Endpoint reference: Endpoints
  • Error format: Errors
  • Interactive docs: /api/v3/docs (Swagger UI), /api/v3/openapi.json

Enabling the API ​

The public API is opt-in. Set the following environment variables on the web backend:

VariableDefaultDescription
EXTERNAL_API_ENABLEDfalseMaster switch for /api/v3/*
EXTERNAL_API_DOCSfalseExpose Swagger UI at /api/v3/docs

Check current state from the UI: Settings -> API (or GET /api/v2/api-keys/status).


Creating an API key ​

  1. Open API & Webhooks -> API keys in the admin UI.
  2. Click Create key. Provide:
    • Name - human-readable label (e.g. prod-monitoring).
    • TTL - Never / 1d / 7d / 30d / 90d / 365d. Expired keys return 401.
    • Scopes - see the table below.
    • Description (optional).
  3. The raw key is shown once. Copy it into your secret store. Closing the dialog without checking "I have saved this key in a secure place" is blocked.
  4. The key prefix (first 12 chars, e.g. rwa_AbCd3fGh) is visible afterwards. The full value is irrecoverable - if lost, rotate or recreate.

Key format ​

rwa_<43-char urlsafe base64>
  • Fixed prefix rwa_ identifies the key class.
  • The random part is secrets.token_urlsafe(32) (32 raw bytes, ~43 base64 chars).
  • Stored as SHA-256(raw_key) hex - the server never holds the plaintext.

Authentication ​

Send the raw key in the X-API-Key header on every request:

bash
curl -H "X-API-Key: rwa_YOUR_KEY" https://panel.example.com/api/v3/users
python
import requests
r = requests.get(
    "https://panel.example.com/api/v3/users",
    headers={"X-API-Key": "rwa_YOUR_KEY"},
    timeout=10,
)
javascript
const res = await fetch("https://panel.example.com/api/v3/users", {
  headers: { "X-API-Key": "rwa_YOUR_KEY" },
})

Failures:

  • Missing header -> 401 {"detail": "Missing X-API-Key header"}
  • Invalid / disabled / expired key -> 401 {"detail": "Invalid or expired API key"}
  • Valid key without the required scope -> 403 {"detail": "Missing scope: <name>"}
  • Rate limit exceeded -> 429 {"detail": "Rate limit exceeded: N requests per minute"} with a Retry-After header.

Scopes ​

Scopes are attached to each key at creation time. A request only succeeds if the key has the scope required by the endpoint.

ScopePurposeSample endpoints
users:readRead user list/detailGET /users, GET /users/{uuid}
users:writeCreate/update/enable/disable/reset-trafficPOST /users, POST /users/{uuid}/enable, POST /users/{uuid}/reset-traffic
users:deleteDelete a single userDELETE /users/{uuid}
nodes:readRead node list/detailGET /nodes, GET /nodes/{uuid}
nodes:writeEnable/disable/restart a node, sync the node list from the panelPOST /nodes/{uuid}/restart
nodes:tokenGenerate/revoke a node's agent tokenPOST /nodes/{uuid}/agent-token/generate
hosts:readRead hostsGET /hosts, GET /hosts/{uuid}
stats:readGlobal statisticsGET /stats
violations:readRead anti-abuse violationsGET /violations, GET /violations/{id}
enforcement:supportReport a customer contact from an external support systemPOST /support-events
bulk:writeBulk operations on usersPOST /users/bulk/*

Guidelines:

  • Grant only what you need. Use users:read for dashboards, not users:delete.
  • Split integrations. A dedicated key per integration makes rotation painless.
  • Prefer short TTL for CI/CD keys (1-7 days) and rotate regularly.
  • bulk:write bypasses per-user scope checks. Treat it as a privileged scope.
  • nodes:token grants access to a node's agent token — the credential that authenticates the Collector API, the agent WebSocket, and remote-terminal commands, effectively a key to the node host. Treat it as more privileged than bulk:write.

A key cannot get a scope whose permission its creating admin does not have. Scopes that work across all users (users:*, bulk:write, violations:read, enforcement:support) can only be granted by an admin without user visibility restrictions. A key of a disabled or deleted admin stops working.


Key restrictions ​

Besides scopes, a key can be narrowed down in the create or edit dialog:

  • Addresses - a list of IPs and subnets, for example 203.0.113.10 and 10.0.0.0/24. From any other address the key is treated as invalid and gets 401. An empty list means any address.
  • Users - squads and a tag. The key only sees and changes users who belong to at least one of these squads and carry this tag (if both are set, both must match), and only their violations. Such a key can create users only in its own squads and with its own tag - the tag is filled in automatically.

Request log ​

Every key has a log of its latest requests in the UI: method, path, response code, IP and time. It shows who uses the key and from where - and when it is time to rotate it.


Rate limits ​

Per-key rate limits are applied via a fixed 60-second window. Defaults (override via env):

Environment variableDefaultApplies to
API_V3_RATE_READ_PER_MIN120GET endpoints
API_V3_RATE_WRITE_PER_MIN60POST / PATCH / DELETE (non-bulk)
API_V3_RATE_BULK_PER_MIN10Anything under /users/bulk/

When exceeded, the server returns 429 with a Retry-After header (seconds remaining in window).

Rate limit counters are keyed by api_key_id, not by IP. Two different keys from the same IP do not share quota.


Rotation ​

  1. In the UI, click the Rotate icon next to the key.
  2. Choose how long the old secret keeps working: revoke it immediately if the key leaked, or keep it for 1-72 hours while the integration switches to the new one.
  3. A new raw key is shown once - update your integration.
  4. No history is lost - id, name, scopes, restrictions, expires_at, description are preserved.

Rotation is logged in the audit trail (api_keys.rotate).


Expiration ​

When a key reaches its expires_at, the server treats it as invalid. The UI shows an Expired badge. Expired keys can be re-enabled only by rotating (new secret, new clock).


Auditing ​

Every create / update / rotate / delete for API keys is written to admin_audit_log with the admin username, IP, and extracted details (name, scopes, description - never the secret).

View from Admin -> Audit log or query admin_audit_log directly.


FAQ ​

  • Why is last_used_at slightly behind? Usage is flushed to the database every 30s (API_KEY_LAST_USED_FLUSH_SEC) to avoid row-lock contention under high RPS.
  • Can I share a key between integrations? Technically yes. Practically no - a single compromised service taints all of them, and audit trails become ambiguous.
  • Can I see the raw key again? No. Rotate or recreate.
  • IP allowlist? Yes - a list of IPs and subnets in the key restrictions.

AGPL-3.0 with a plugin exception