API reference
Authentication, the 401 challenge, the api.shebang.pro rewrite, and the handful of endpoints that don't belong to one service.
API reference
Every route under /api/<service>/v1/... (and /api/agent-login/...,
/api/mcp, /api/oauth/decision) is reachable two ways:
- Directly on the dashboard host —
https://dash.shebang.pro/api/<service>/v1/.... - Through the platform API host —
https://api.shebang.pro/v1/<service>/..., a plain rewrite onto the same route (see The api.shebang.pro rewrite below).
api/account/** (the dashboard's own signed-in session routes) isn't part
of this reference — it's reachable only from a browser holding a dashboard
session cookie, never from an API key or an MCP client.
Authentication
Every /v1/... route accepts an Authorization: Bearer <credential>
header carrying one of two things:
- An
shb_...key — minted from the dashboard's account page orkey_create_app/the device-login flow. Has a tier (masterorapp) and, for an app key, anappsscope list (page,serve,link,base) — see Keys. - A sherlock OAuth bearer token — a JWT issued to a registered OAuth
client through sherlock's authorization-code flow. Verified against the
issuer, the
authenticatedaudience, the ES256 algorithm, anamr: oauth_provider/authorization_codeclaim, and a live client registration (checked every 60 seconds) — a token missing any of those is rejected the same as an unrecognized key. The client's home project (if any) sets the token's scope, exactly like an app key bound to that project; a client attached to no project acts with full account authority, like a master key.
Both forms are checked by the same code path, so a route never knows or cares which kind of credential it received.
The 401 challenge
A missing, unrecognized, or revoked credential gets:
401 { "error": "invalid_api_key" }
WWW-Authenticate: Bearer resource_metadata="https://api.shebang.pro/.well-known/oauth-protected-resource"
A credential that looked like a bearer token (JWT-shaped) but failed verification gets a more specific body and header naming why:
401 { "error": "invalid_token", "error_description": "token expired" }
WWW-Authenticate: Bearer error="invalid_token", error_description="token expired", resource_metadata="..."
error_description is one of: token expired, unknown signing key,
token issuer or audience mismatch, not an OAuth-obtained token,
unknown or deleted client, client registry unavailable, or
malformed bearer token.
A valid credential lacking the scope, tier, or project access a route
requires gets 403 { "error": "insufficient_scope" } instead — never a
different status, so a caller can always tell "this credential doesn't
work" (401) apart from "this credential works, but not for this" (403).
The resource_metadata URL above is GET /api/.well-known/oauth-protected-resource
— RFC 9728 protected-resource metadata, no authentication required, see
Other endpoints below for its response shape.
The api.shebang.pro rewrite
A request to api.shebang.pro is rewritten before anything else runs:
Requested on api.shebang.pro |
Rewritten to (on dash.shebang.pro) |
|---|---|
/v1/platform/... |
/api/platform/v1/... |
/v1/sherpage/... |
/api/sherpage/v1/... |
/v1/sherserve/... |
/api/sherserve/v1/... |
/v1/sherlink/... |
/api/sherlink/v1/... |
/v1/sherbase/... |
/api/sherbase/v1/... |
/mcp |
/api/mcp |
/.well-known/oauth-protected-resource |
/api/.well-known/oauth-protected-resource |
So POST https://api.shebang.pro/v1/sherpage/pages and
POST https://dash.shebang.pro/api/sherpage/v1/pages are exactly the same
call — same auth, same body, same response. /db/<slug>/... isn't part
of this rewrite at all — the reverse proxy in front of both hosts routes
that path straight to the sherbase Data API gateway before the request
ever reaches the platform API, so the rewrite above never sees it. See
the Data API for what lives there. Anything
else requested on api.shebang.pro gets a plain 404 { "error": "not_found" } from the rewrite itself, before reaching any route.
Other endpoints
GET /api/.well-known/oauth-protected-resource
RFC 9728 protected-resource metadata — the document every
WWW-Authenticate header above points at.
- Auth — none.
- Response —
200,Cache-Control: public, max-age=300:{ "resource": "https://api.shebang.pro", "authorization_servers": ["https://auth.shebang.pro/auth/v1"], "bearer_methods_supported": ["header"], "scopes_supported": [] }scopes_supportedis deliberately empty — sherlock only issues the fixedopenid profile email phonescopes, so this document can't advertise service-level scopes without lying about what a token actually carries.
POST /api/mcp
The hosted MCP server (streamable HTTP transport, one request/response per call, no session state kept between calls).
- Auth — same bearer credential as every
/v1/...route (shb_...key or sherlock bearer). The credential is forwarded unchanged to the/v1/...routes the MCP tools call internally — a tool call carries exactly the authority the original credential carries, no more. - Request body — a single MCP JSON-RPC message (
initialize,tools/list,tools/call, ...). - Response —
200with the MCP JSON-RPC response. - Errors —
401 invalid_api_key/401 invalid_token, same shapes and sameWWW-Authenticateheader as any/v1/...route.
POST /api/oauth/decision
Backs the sherlock OAuth consent screen's Approve/Deny buttons — not an
API a caller ever calls directly; documented here because it lives under
/api/oauth/**.
- Auth — a signed-in dashboard session cookie (not a bearer credential), with the second factor already satisfied if the account has one enrolled.
- Request body — form-encoded
authorization_id,decision(approveordeny). - Response —
303redirect to theredirect_urlsherlock returns. - Errors —
403 { "error": "invalid origin" }(cross-site form submission),401 { "error": "not signed in" },403 { "error": "mfa_required" }(second factor not yet satisfied this session),400 { "error": "invalid request" }(malformedauthorization_idordecision),400 { "error": <message> }(sherlock rejected the decision itself).
Next
Platform — projects, keys, OAuth clients, and email.