---
title: API reference
description: 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](#the-apishebangpro-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](/concepts/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](#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](/sherbase/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`:
  ```json
  {
    "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`, ...).
- **Response** — `200` with the MCP JSON-RPC response.
- **Errors** — `401 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`).
- **Response** — `303` redirect to the `redirect_url` sherlock 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" }` (malformed `authorization_id` or
  `decision`), `400 { "error": <message> }` (sherlock rejected the
  decision itself).

## Next

[Platform](/api/platform) — projects, keys, OAuth clients, and email.

