Skip to main content

GET /openapi.json

Returns a generated OpenAPI 3.0 document describing every registered public route — no authentication required, since it carries no data, only shape.
This is a route catalog, not a hand-authored contract: paths, HTTP methods, and path parameters ({id}-style) are exact, generated straight from the route registrations in internal/api by cmd/gen-openapi — from every file in that package that registers a route, whatever it is called. Bodies are derived from the handlers, not authored: most operations now carry a named schema, and where the generator could not derive a shape it emits an unconstrained object rather than guessing. For prose, examples and the reasoning behind a field, use the per-resource pages in this section. /api/v1/internal/* routes are deliberately excluded — that surface is sidecar-only, X-Internal-Token authenticated (see Internal IPC), and this endpoint itself carries no auth. Publishing a route map of the one part of the API kept non-public would be counterproductive.

Browse it from your instance

The same document is rendered as browsable HTML by the instance itself:
The point is narrower than “nicer docs”. The spec is the discovery surface an agent drives the API from, so a rendering is how a human checks what the agent is seeing — against their instance and their build, not against whatever this docs site last published. Three pages:
  • /openapi — every operation, grouped by tag, with a filter box. Operations that answer without credentials are marked public.
  • /openapi/op/{operationId} — one operation: which credentials it accepts, its parameters split by location (path, query, header) with required shown as binding or not proven required, and its request and response schemas with $refs resolved and linked.
  • /openapi/schemas and /openapi/schema/{name} — the component schemas, each with the operations that name it. Schemas no operation reaches are flagged unreferenced.
It is self-contained: no CDN, no remote font, no third-party JavaScript. The binary embeds the whole rendering, so it works on an offline or air-gapped install, and the server serves it under a default-src 'none'; script-src 'self'; style-src 'self' Content-Security-Policy — a remote asset added there by accident would not load.
Like /openapi.json, this surface is unauthenticated: it renders the same public route catalog and carries no data. If you would not want the route map of your instance readable, that was already true of the JSON.
It does not replace these reference pages. The generated document carries no prose — no summaries, no descriptions, no examples — so the rendering shows shape and status, and this section stays the place where behaviour is explained. There is no “try it out”: nothing on that page issues a request.

Query parameters

Query parameters are inferred from the handler body — the generator resolves each registration to the concrete handler it runs and lists the names that handler reads off the request. A parameter it cannot attribute to a handler is left out rather than guessed in, so the list is a subset of what an operation accepts, never a superset. That subset has one blind spot worth knowing about: a handler that parses nothing itself and calls a shared helper to do it. GET /api/v1/paymaster/spend/by-crew reads its window through parseWindow, GET /api/v1/journal its whole filter grammar through parseJournalQuery. The scan reads one function body and stops, so those routes carry an explicit // openapi: annotation naming what the helper accepts. If an operation lists no query parameters at all, it may genuinely take none — or its annotation may still be missing. required is inferred the same way, and asymmetrically:
  • required: true means the handler was seen to answer 4xx when the parameter is absent — for example DELETE /api/v1/crews/{crewId}/files/delete?path=, which answers 400 path parameter required. Send it.
  • required: false means not proven required, not proven optional. Most parameters are genuinely optional; a few are required in ways the inference does not recognise (an obligation that depends on another parameter, or a value validated by a helper the generator will not follow into).
Over-claiming would tell a client a request will fail when it will succeed, so the generator under-claims by design. Treat required: true as binding and required: false as unknown. A parameter can also be declared required by hand, with a ! in the route’s annotation (// openapi: query metric:string!). That is reserved for handlers whose check is delegated — GET /api/v1/metrics/timeseries rejects an absent ?metric inside parseTimeseriesParams, GET /api/v1/auth/pair/poll an absent ?code inside normalizePairingCode. Declared and inferred entries are held to the same standard: each is pinned by name in cmd/gen-openapi’s tests and each has a test showing the 4xx when the parameter is omitted, so required: true means the same thing however it got there.

Methods not listed are rejected

If the spec lists only GET for a path, every other method on that path answers 405 Method Not Allowed with an RFC 9110 Allow header naming the methods that do work:
This used not to hold. Go’s router matches method and path together, so an unlisted method on a literal path could fall through to a sibling parameterised route — DELETE /api/v1/notifications/count reached the delete-notification handler with id="count" and answered 401/404 instead of 405. Twenty-nine paths behaved that way. (That particular pair of routes has since been removed — see #1751 — but the failure mode it illustrates is why the guards exist.) A client can now trust the spec’s method list: what it does not list, the server refuses.
This used to say that bodies were a generic placeholder and that every operation documented only 200. Neither is true any more: the document enumerates the error statuses each operation can answer (400, 401, 403, 404, 409, 429, 500, …) alongside its success status.574 of 589 operations return a named 2xx schema, 0 fall back to an unconstrained object, and 15 have no success body at all. 251 operations carry a request body: 247 with a named JSON schema, 4 with a non-JSON media type, and 0 with a generic JSON fallback.The spec is authoritative for paths, methods and path parameters; treat a derived body as a strong hint and this reference section as authoritative for meaning.
Before this endpoint existed, GET /openapi.json returned 200 text/html — the embedded Next.js SPA answers every unmatched path with its index page. A status-code check alone can’t tell that apart from a real spec; check the Content-Type is application/json.
Regenerate after adding or renaming a route: