External access to API
The Remnawave panel can serve its API to external clients: bots, management panels, statistics tools. By default the script hides everything behind the cookie gate or a login page, so only what you open yourself goes outside: one location in nginx.conf, and from then on every request passes the same protection as the browser, plus the panel token (Settings → API tokens).
Opening the API to the outside
Section titled “Opening the API to the outside”The block is the same for every install type (“panel only” and “panel + node”).
-
Open the nginx configuration:
root@server: ~ nano /opt/remnawave/nginx.conf -
In the server block with the panel domain (
server_name panel.example.com;) add alocation: it takes/api/out from under the cookie check and hands requests straight to the panel:nginx.conf location ^~ /api/ {proxy_http_version 1.1;proxy_pass http://remnawave;proxy_set_header Host $host;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection $connection_upgrade;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;proxy_set_header X-Forwarded-Proto $scheme;proxy_set_header X-Forwarded-Host $host;proxy_set_header X-Forwarded-Port $server_port;proxy_send_timeout 60s;proxy_read_timeout 60s;} -
Restart the web server so the configuration applies:
root@server: ~ cd /opt/remnawave && docker compose restart remnawave-nginx -
Check that it answers from the outside (a request without a token must return an authorization error, not a timeout):
root@server: ~ curl -i https://panel.example.com/api/nodes
Caddy comes in two flavors, and the API works differently in each.
MFA portal (caddy-with-auth). Nothing to add: the route /api/* path is already open in the stock Caddyfile. The API is protected by its own token, panel login stays behind the portal, and programmatic clients use the X-Api-Key (see the “MFA portal” way below).
Cookie gate. The API is closed together with the panel: a request without the cookie runs into an abort. To open it, add a separate route.
-
Open the Caddyfile:
root@server: ~ nano /opt/remnawave/Caddyfile -
Inside the
https://{$PANEL_DOMAIN}block add a route at the top, before the@has_token_paramand@unauthorizedmatchers:Caddyfile route /api/* {reverse_proxy {$BACKEND_URL} {header_up X-Real-IP {remote}header_up Host {host}}} -
Restart Caddy:
root@server: ~ cd /opt/remnawave && docker compose restart remnawave-caddy -
Check that it answers from the outside (a request without a token must return an authorization error, not a timeout):
root@server: ~ curl -i https://panel.example.com/api/nodes
Passing the protection
Section titled “Passing the protection”From here everything depends on how the panel is protected, chosen at installation. The panel token (Authorization: Bearer …) travels in every request on top of any of the options below.
Getting the cookie
Section titled “Getting the cookie”The script generates a random NAME=VALUE pair. Open nginx.conf and find the map $http_cookie $auth_cookie line; your data is the value after ~*:
map $http_cookie $auth_cookie { default 0; "~*zIdejKtN=MWbLrRzx" 1;}Here the cookie is zIdejKtN=MWbLrRzx.
Sending the cookie in a header
Section titled “Sending the cookie in a header”Using the HTTP client httpx and the COOKIES entry in your project’s .env as an example:
COOKIES={"zIdejKtN":"MWbLrRzx"}token = os.getenv("API_TOKEN", "")base_url = os.getenv("REMNAWAVE_BASE_URL", "")cookies = json.loads(os.getenv("COOKIES", "{}"))
async def get_all_nodes(): headers = { "Content-Type": "application/json", "Authorization": "Bearer " + token }
async with httpx.AsyncClient(cookies=cookies) as async_client: response = await async_client.get(f"{base_url}/api/nodes", headers=headers)
return response.json()A quick curl check:
curl -b "zIdejKtN=MWbLrRzx" -H "Authorization: Bearer <token>" https://panel.example.com/api/nodesLogin-page credentials go in the separate X-Api-Key header, while Authorization with your panel token passes through to the API untouched; both headers travel in the same request.
X-Api-Key carries the TinyAuth credentials as Basic base64(username:password). Build the pair with:
printf '%s' 'username:password' | base64# dXNlcm5hbWU6cGFzc3dvcmQ=Resulting request headers:
{ "Authorization": "Bearer <panel token>", "X-Api-Key": "Basic dXNlcm5hbWU6cGFzc3dvcmQ="}A quick curl check:
curl -H "X-Api-Key: Basic dXNlcm5hbWU6cGFzc3dvcmQ=" -H "Authorization: Bearer <token>" https://panel.example.com/api/nodesThe key is issued by the portal itself at https://<panel domain>/r/settings/apikeys (the “API Keys” link). Send it as is, without the Basic wrapper:
{ "Authorization": "Bearer <panel token>", "X-Api-Key": "<portal-issued key>"}A quick curl check:
curl -H "X-Api-Key: <portal-issued key>" -H "Authorization: Bearer <token>" https://panel.example.com/api/nodesStandalone subscription page
Section titled “Standalone subscription page”The “subscription page only” install asks how the panel is protected and fills everything in for you: with TinyAuth you just enter the login page username and password, the installer assembles the base64 pair itself.
| Panel protection | What the installer asks | Variable in the sub page compose |
|---|---|---|
| Cookie gate | the NAME=VALUE cookie |
EGAMES_COOKIE=NAME=VALUE |
| Login page (TinyAuth) | username and password | CADDY_AUTH_API_TOKEN=Basic base64(...) |
| Caddy with MFA | the login page API key | CADDY_AUTH_API_TOKEN=<key> |
