shebang!

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 hosthttps://dash.shebang.pro/api/<service>/v1/....
  • Through the platform API hosthttps://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 or key_create_app/the device-login flow. Has a tier (master or app) and, for an app key, an apps scope 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 authenticated audience, the ES256 algorithm, an amr: oauth_provider/authorization_code claim, 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.
  • Response200, 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_supported is deliberately empty — sherlock only issues the fixed openid profile email phone scopes, 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, ...).
  • Response200 with the MCP JSON-RPC response.
  • Errors401 invalid_api_key / 401 invalid_token, same shapes and same WWW-Authenticate header 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 (approve or deny).
  • Response303 redirect to the redirect_url sherlock returns.
  • Errors403 { "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" } (malformed authorization_id or decision), 400 { "error": <message> } (sherlock rejected the decision itself).

Next

Platform — projects, keys, OAuth clients, and email.