Files
proxy-manager/docs/npm-api.md
T
pastilhasandClaude Fable 5 5914e96cf3 proxy-manager: nginx proxy manager in officer
reads hosts and certificates against a live NPM 2.15.1. one grantable
permission, with /_config owner-only — reading which hosts are healthy
is worth sharing, an NPM admin login is not, and NPM has no read-only
role.

the quirk that shapes the whole plugin: NPM answers an unauthenticated
read with 404, not 401, because its router only registers a resource
once a token is present. so 'not signed in' and 'no such endpoint' are
the same response, and a naive client tells you a feature is missing
when your password is wrong. the client mints a fresh token and retries
once, then reports a persistent 404 as likelyAuth.

certificates sort by expiry rather than name, which is the only ordering
that answers a question.

no create/edit/delete and no cert issuance: NPM's own UI does those and
a wrong proxy host takes a domain off the internet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 08:38:15 +00:00

139 lines
4.3 KiB
Markdown

# Nginx Proxy Manager's API, as observed
Verified against **2.15.1** on alpha, 2026-08-25. NPM's API is not documented upstream in any form worth
relying on, so this is what was learned by probing it — recorded so nobody has to probe it twice.
---
## Authentication
```
POST /api/tokens {"identity": "you@example.com", "secret": "…"}
-> {"token": "eyJ…", "expires": "2026-08-26T10:00:00.000Z"}
```
Then `Authorization: Bearer <token>` on everything else.
**A bad credential is 400, not 401**, with `{"error":{"code":400,"message":"Invalid email or password"}}`.
**An unauthenticated read is 404, not 401.**
```
GET /api/nginx/proxy-hosts
-> {"error":{"code":404,"message":"Not Found - /nginx/proxy-hosts"}}
```
This is the single most important thing on this page. NPM only registers a resource route once the
request carries a token, so *"not signed in"* and *"no such endpoint"* are indistinguishable. Any client
that reports the status honestly will tell its user a feature is missing when the password is wrong.
`api/npm-client.ts` handles it by minting a fresh token and retrying once, then reporting a persistent
404 as `likelyAuth`.
## The unauthenticated corner
`GET /api/` needs no token and is the cheapest proof a URL is an NPM at all:
```
{"status":"OK","setup":true,"version":{"major":2,"minor":15,"revision":1}}
```
`setup: false` means NPM has not had its first admin created yet.
## Error shape
Always the same, which is a small mercy:
```json
{ "error": { "code": 404, "message": "Not Found - /nginx/proxy-hosts" } }
```
---
## Resources
| path | |
| --- | --- |
| `/nginx/proxy-hosts` | the main one |
| `/nginx/redirection-hosts` | 301/302 redirects |
| `/nginx/dead-hosts` | NPM's name for "404 hosts" — a domain served deliberately as nothing |
| `/nginx/streams` | raw TCP/UDP forwarding |
| `/nginx/certificates` | Let's Encrypt and uploaded |
| `/nginx/access-lists` | basic auth and IP rules |
| `/audit-log` | who changed what |
| `/settings`, `/users` | administration |
### `?expand=`
Several resources take an expand list, which is how you avoid N+1 fetching:
```
/nginx/proxy-hosts?expand=certificate,access_list
/nginx/proxy-hosts/3?expand=certificate,access_list,owner
/nginx/access-lists?expand=items,clients
```
### A proxy host, narrowed to the useful fields
```jsonc
{
"id": 3,
"domain_names": ["gitea.officer.dev"],
"forward_scheme": "http",
"forward_host": "172.20.0.3",
"forward_port": 3000,
"enabled": 1, // 0/1, not a boolean
"ssl_forced": 1,
"certificate_id": 7, // 0 means none
"access_list_id": 0, // 0 means none
"meta": {
"nginx_online": true, // whether nginx accepted the generated config
"nginx_err": null
}
}
```
**`meta.nginx_online` is the interesting one.** A host can be enabled and still be offline because the
generated nginx config failed to load — a certificate that vanished, a syntax error from a custom
config block. It is the difference between "should be running" and "is running", and it is what the
hosts panel shows as a warning triangle.
Booleans are `0`/`1` integers throughout.
### Enabling and disabling
```
POST /api/nginx/proxy-hosts/:id/enable
POST /api/nginx/proxy-hosts/:id/disable
```
No body. This is the only mutation this plugin makes, because it is the only one that is reversible in
a click.
### Certificates
```jsonc
{
"id": 7,
"provider": "letsencrypt", // or "other" for an uploaded one
"nice_name": "officer.dev",
"domain_names": ["officer.dev", "*.officer.dev"],
"expires_on": "2026-11-01T00:00:00.000Z"
}
```
`expires_on` is null for some uploaded certificates. The router computes `daysUntilExpiry` server-side
so the browser is not parsing dates and getting the timezone wrong.
---
## Reaching NPM from Officer
On alpha, NPM is the `npm` container on the `nginx` docker network at **172.20.0.2**, with its admin UI
published on `100.64.0.13:81` and `127.0.0.1:81`.
Both `http://127.0.0.1:81` and `http://172.20.0.2:81` were probed and both answer. The container address
works **whether or not the port is published**, because the host is the gateway on every docker bridge —
`opm`'s `docs/networking.md` has the measurements. So the connection URL can be the container address,
and the published port on 81 becomes an exposure decision rather than a requirement.