GET /admin/openapi.json
The reference these pages point at: an OpenAPI 3.1 description of every route
above, generated from the same route table the router registers from, so the two
cannot drift. It takes no {tenant} — it describes the API rather than reaching
into one — and is served without authentication, since a client that cannot read
it cannot generate against it.
curl -sS -D - -o openapi.json http://localhost:3197/admin/openapi.json
jq '.paths | length' openapi.json
HTTP/1.1 200 OK
x-request-id: 01a16544-dcb5-736e-be4a-79fdff009ee5
access-control-allow-origin: *
content-type: application/json; charset=utf-8
content-length: 432312
Date: Thu, 22 Oct 2026 20:19:34 GMT
Connection: keep-alive
Keep-Alive: timeout=72
78
422 KB and 78 paths, which is the whole route table. It is the one admin
response readable from any origin, so a viewer served from another port can load
it — the local stack's optional Swagger UI does exactly that (see Browsing the
admin API). No admin route carries that header, so a page on
another origin can read the description but cannot call the API with it. Its
first bytes, and the bearerAuth scheme it declares — head -c 180 openapi.json and the substring at securitySchemes:
{"openapi":"3.1.0","info":{"title":"Odudu admin API","version":"0.0.0"},"security":[{"bearerAuth":[]}],"paths":{"/admin/tenants/{tenant}/whoami":{"get":{"summary":"Requires an auth
"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"An access token whose \"aud\" claim names urn:odudu:params:admin-api. A token minted for another audience, including the protocol surface itself, is refused with 401."}}
security is declared once at the top level, so every path inherits it
rather than repeating it. /admin/openapi.json is not among those 78
paths: the document does not describe itself, which is why serving it
unauthenticated does not contradict the blanket security above.
Each operation also carries its requestBody, generated from the same body
schema the router validates a request against, and its query parameters,
generated from the same querystring schema — before, the document declared
neither, so a generated client had the path and the response shape but not
what to send.