Documentation for v2.9.0. Read the latest, v2.12.0.
Reference
Using the API
The two generated references list every route: the REST API that build
plugins and CLIs speak (api.yaml) and the
Maintenance API behind the admin pages
(maintenance.yaml). This page is the prose around them: how a call authenticates,
what a token may do, and what an error looks like.
In one minute#
-
Provide:
POST /providefor OpenAPI,/provide/asyncapiand/provide/grpcfor the other types, with a declaredstability; the version is read from the spec itself. -
Require:
GET /requirefor one endpoint orPOST /require-bundlefor several, always at an exact pinned version. -
Authenticate with
Authorization: Bearer <token>. The header is the only place a token is read from, never a query parameter, so tokens do not end up in logs, history or referrers. -
Validate for free:
POST /validateruns the provide-side checks without authentication and without storing anything.
Sessions and API tokens#
Sanshain knows two credentials, and both travel in the same header. A
session token is what POST /auth/login answers ({"token": "…"}) and what the web UI sends with every call; it expires with the session. An
API token is long-lived, belongs to one user and is meant for CI, scripts and AI
clients. Users mint their own on the account page (/account.html) or with
POST /auth/tokens; the raw token is shown once, expires_in_days is mandatory, and the listing shows
when each token last authenticated a request.
Which credential a route takes is stated on every entry of the generated pages:
| Routes | Session token | API token |
|---|---|---|
Provide, require, catalog push and version listings (REST API), the
reports, /endpoint-versions and the update streams
|
accepted | accepted; the scope decides what it may do |
/auth/tokens (minting and revoking tokens) |
required | refused: a token cannot mint tokens |
Maintenance API (/admin/*) and the UI's own
endpoints (/auth/*, the dashboard, the audit timeline)
|
required |
refused with 401 and an ErrorBody, whatever the scope: these
routes validate sessions only. The one exception is
GET /admin/backup/sqlite, which also admits a full token for the scheduled backup and refuses a
read_only one with 403.
|
/mcp (AI assistants) |
refused | required; prefer read_only |
/validate, /version, /health, /ready
|
no authentication | |
Scopes: full and read-only#
A token has a scope, decided when it is minted and immutable afterwards (ApiTokenScope):
| Scope | May | Refused with 403 |
|---|---|---|
full |
Everything its owner may do. | — |
read_only |
GET /report and its variants,
/producers/{producername}/versions, /endpoint-versions, the SSE and WebSocket update streams, the MCP
endpoint.
|
Provide (all types, including retire), /require,
/require/asyncapi, /require/grpc, /require-bundle,
PUT /catalog/{systemname}, and GET /admin/backup/sqlite. The body names the token's scope and the
scope the call needs.
|
The reference marks these routes session or API token (full); routes a
read-only token may call are marked
session or API token (full or read-only). Tick
Read-only (for AI / MCP clients) on the account page, or send
"scope": "read_only", to mint one.
Independently of the scope, publishing a GA version (stability: ga) needs the
release_ga permission of the token's owner, the releaser role; admins
and root hold it implicitly. Retiring a family needs the same permission or maintainership of the
Producer. On the maintenance side the contract declares, per operation, the permission it needs
(x-permission) and whether a Producer's maintainers are admitted too
(x-scope); the reference shows both, and a build-time test keeps the fields equal to
the router's guards.
Error shapes#
Errors raised by the application (AppError) answer JSON in the shape of
ErrorBody: error carries the human-readable message, proposed_version appears
only on a 409 from the version rules and is the next free version to publish as
instead. Not every refusal reaches the application; the exceptions are listed with their status
below.
{ "error": "GA version 1.2.0 exists with different content", "proposed_version": "1.3.0" }
-
400: the request is understood but wrong: a malformed spec, a bad version, an instant that is not RFC 3339. -
401: the token or session sent is not valid. The maintenance routes answer anErrorBody; the API routes and/auth/tokensanswer a bare status with no body (the authentication guards, not the application, refuse). An API call with noAuthorizationheader at all is refused with a bare403. -
403: signed in, but refused: a read-only token on a write route, a GA provide withoutrelease_ga, or a missing administrative permission. All three carry anErrorBody. -
404versus410on require: the Producer or the pinned version does not exist (a pin configuration error) versus the version exists and deliberately does not contain the endpoint. 409: the version rules refused a provide; fix the version and republish.-
413: the body exceedsMAX_SPEC_BODY_BYTES(default 4 MiB). Axum's body limit answers it before any handler runs; the body is plain text. -
422: the JSON body has the wrong shape, for example a missingstabilityor a 1.x field such asbranch, which is rejected by name. The body is the deserializer's plain-text message, for examplemissing field `stability`. -
503: authentication is switched off in maintenance mode; the body is plain text.
A successful call can still carry a Sanshain-Warning header, once per warning, for
things a build should print without failing: an unknown tag, trunk and
tag together, a catalog push that recorded nothing.
Limits and caching#
-
REST routes are not rate limited; the request body limit (
MAX_SPEC_BODY_BYTES) is the only ceiling. -
The MCP endpoint allows 60 tool calls per minute per API token
(
MCP_CALLS_PER_MINUTE, a token bucket that refills continuously);initializeandtools/listdo not count. Over the limit, a tool answers a result withisError: trueandrate limited - retry in N s, never an HTTP 429, so the client's session survives. One tool answer is capped at 64 KiB (MCP_MAX_RESULT_BYTES); larger answers come back as a summary with a hint. See MCP for AI assistants. -
Every require route supports
ETagandIf-None-Match; unchanged content answers304 Not Modified. Responses carryX-Sanshain-Version(the pin) andX-Sanshain-Stability(what actually answered,gaorsnapshot).
Reports and graph scopes#
GET /report returns the dependency report; ?scope=main[@instant] or
?scope=<branch>[@instant] replaces the graph with that pin set
(dev, the default, is the accumulated activity). The same parameter applies to
/report/markdown and /report/isolation. The report routes are not in
api.yaml (see below), so this parameter is documented only here and in
The web UI.
Not in the contract#
One route takes an API token and is documented outside api.yaml:
POST /mcp, API token only, in
MCP for AI assistants. Everything else a token can reach
(reports, the endpoint history, the update streams) is in the REST reference.
Names#
The two roles are Producer (provides a spec) and
Consumer (requires endpoints). Only the canonical field names
producername and consumername are accepted; the 1.x aliases
servicename and clientname, and the branch-era fields
branch, base_version, force, timeout and
pull_from_branch, are rejected as unknown fields.
Admin routes are not aliased either: /admin/services* became
/admin/producers*, /admin/clients* became
/admin/consumers*, and /admin/nuke/services /
/admin/nuke/clients became /admin/nuke/producers /
/admin/nuke/consumers. The old paths answer 404.
Developer Mode#
For local testing only, Developer Mode bypasses authentication; it needs both the switch and the
ALLOW_INSECURE_DEV_MODE=true safety gate and fails closed otherwise. See
Developer mode.