API and MCP
Deeperer ships a public REST API and an MCP endpoint on every tier, free included. The two surfaces share state, a key, and the same run path. The OpenAPI document at /api/v1/openapi is the canonical machine-readable contract.
Get an API key
API keys belong to a workspace. Anyone with admin access to a workspace can create or revoke them from the dashboard.
- 01Sign in to the dashboard and choose the workspace the key should belong to.
- 02Open Settings, then the API keys tab.
- 03Create a key. The full token is shown once at creation; copy it then. Lose the token, revoke and reissue.
Keys ship a structured prefix that public secret scanners recognise, so leaked tokens posted to a public Git repository are detected automatically. Keys store as a hash on the server; a revoked key stops authorising the next request.
Authentication
Every request carries a workspace-scoped API key in the Authorization header. Keys are created from the dashboard Settings tab and revoked the same way. The foundation ships static API-key authentication only; an OAuth flow is the subject of a future authorization spec and is not part of foundation.
Scope vocabulary
reports:read- read reports, versions, and version bodies.reports:write- reserved for future writer endpoints. No public route consumes it in foundation.runs:start- trigger a new generation run on an existing report.
Missing or revoked key
A request without a valid key returns 401 with a WWW-Authenticate header per RFC 9728. The header carries the realm and the scope the requested route requires, so a client can request the right scope on retry.
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="deeperer-api-v1", scope="reports:read"
{ "error": "Authentication required.", "code": "authentication_required" }A key whose scopes do not cover the requested route returns 403 with the same header shape and error="insufficient_scope".
HTTP/1.1 403 Forbidden
Content-Type: application/json
WWW-Authenticate: Bearer realm="deeperer-api-v1", scope="runs:start", error="insufficient_scope"
{ "error": "Insufficient scope.", "code": "insufficient_scope" }MCP discovery
The MCP endpoint at /api/mcp uses the realm string deeperer-mcp and otherwise follows the same 401 / 403 shape. Its challenge additionally carries resource_metadata, pointing at /.well-known/oauth-protected-resource/api/mcp - an RFC 9728 document naming the resource, the scopes it accepts and the fact that the key travels in the header. It advertises no authorization server, because there is none: the document is discovery, not a token flow.
REST endpoints
Four public routes ship under /api/v1. The full machine-readable surface lives at /api/v1/openapi as an OpenAPI 3.1 document, including the full status code union per route. Administrative paths are never enumerated there.
| Method | Path | Scope |
|---|---|---|
| GET | /api/v1/reports | reports:read |
| GET | /api/v1/reports/{id} | reports:read |
| GET | /api/v1/reports/{id}/versions | reports:read |
| POST | /api/v1/reports/{id}/runs | runs:start |
List reports
List reports in the calling key's workspace. Cursor pagination via limit (1 to 100) and cursor.
curl -H "Authorization: Bearer deep_sk_live_..." \
"https://deeperer.com/api/v1/reports?limit=20"Fetch one report
Fetch a report with its latest version body.
curl -H "Authorization: Bearer deep_sk_live_..." \
"https://deeperer.com/api/v1/reports/<report-uuid>"List versions
List versions of a report, freshest first.
curl -H "Authorization: Bearer deep_sk_live_..." \
"https://deeperer.com/api/v1/reports/<report-uuid>/versions"Trigger a run
Trigger a generation run on a report. A report whose run is already under way is joined rather than refused - the answer reports entry as joined and no second run opens. Requires an Idempotency-Key header; a retry with the same key returns the original response. Send expected_version_id to name the version you are starting from: the start is refused when the report has moved past it. The response is 202 with { report_id, slug, state: "generating", entry } where entry is started or joined; poll the report read endpoint for completion.
curl -X POST \
-H "Authorization: Bearer deep_sk_live_..." \
-H "Idempotency-Key: a-stable-string-the-client-controls" \
-H "Content-Type: application/json" \
-d '{"expected_version_id": "<version-uuid>"}' \
"https://deeperer.com/api/v1/reports/<report-uuid>/runs"MCP endpoint
The Model Context Protocol endpoint at /api/mcp exposes the same primitives over Streamable HTTP, the canonical MCP TypeScript SDK transport. Point any client at https://deeperer.com/api/mcp with a workspace key. Tool names are stable; schema evolves additively and breaking changes ship as side-by-side parallel tools rather than version suffixes.
| Tool | Scope | Effect | Summary |
|---|---|---|---|
| list-reports | reports:read | reads only | List reports in the calling key's workspace. |
| get-report | reports:read | reads only | Fetch one report with its latest version body. |
| get-report-markdown | reports:read | reads only | Fetch one report as markdown, the document its published markdown representation serves. |
| list-versions | reports:read | reads only | List versions of a report, freshest first. |
| get-version | reports:read | reads only | Fetch a single version body by id. |
| start-run | runs:start | spends | Trigger a generation run, joining the run the report already has rather than refusing. Pass a stable idempotency_key to dedupe retries, and expected_version_id to name the version you are starting from. |
Resources and prompts
The endpoint also publishes reports as resources under deeperer://reports/{id}, so a client can browse the workspace without calling a tool. That listing carries the 50 newest reports the key may read; every other one stays readable at its own resource address. It ships ready-made prompts too: summarise-report (read a report and summarise what it found, sources named.), refresh-report (re-run a report, after confirming the spend with the operator.).
Discover the tool set
The Accept header is required: Streamable HTTP refuses a POST that does not list both application/json and text/event-stream.
curl -X POST \
-H "Authorization: Bearer deep_sk_live_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }' \
"https://deeperer.com/api/mcp"Connecting without a key
An unauthenticated call answers the standard challenge, pointing at a document that states what protects the endpoint - which scopes exist, that the key travels in the header, and that no authorization server stands behind it. Authentication is a static workspace key; there is no delegated-authorization flow to enter.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="deeperer-mcp", scope="reports:read", error="invalid_request", resource_metadata="https://deeperer.com/.well-known/oauth-protected-resource/api/mcp"
{ "error": "Authentication required.", "code": "authentication_required" }Trigger a run from MCP
The start-run tool routes through the same kernel the REST run endpoint uses. Both paths converge on one set of state transitions, so a client can mix transports without splitting state.
curl -X POST \
-H "Authorization: Bearer deep_sk_live_..." \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "start-run",
"arguments": {
"id": "<report-uuid>",
"idempotency_key": "a-stable-string-the-client-controls",
"expected_version_id": "<version-uuid>"
}
}
}' \
"https://deeperer.com/api/mcp"Rate limits
Every authenticated request runs against two caps in series. The first is per IP and bounds the rate from a single network origin. The second is per API key. A request fails the slower of the two; rotating keys does not bypass the per-IP cap.
Successful responses carry the three standard headers. The counter lives in a durable cache, not a database hot row.
X-RateLimit-Limit- the ceiling for the active window.X-RateLimit-Remaining- requests left before the cap engages.X-RateLimit-Reset- Unix epoch seconds when the window resets.
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1735689600Once the cap engages, the response is 429 with the same three headers; the retry hint is X-RateLimit-Reset (Unix epoch seconds when the window resets). The body carries the scope that engaged the cap so a client can attribute the rejection to its key or its network origin.
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1735689600
{ "error": "Too many requests", "code": "rate_limited_ip" }Questions reach the team at in@deeperer.com. Back to landing.