Skip to content

HTTP API Overview

The WarmHub HTTP API exposes mounted endpoints for repository reads, action observability, callbacks, MCP HTTP transport, and SSE. Most endpoints return JSON; the synchronous export stream returns application/x-ndjson (see Response Format).

Repository data writes and management operations are not REST CRUD endpoints. Use the SDK, CLI, or MCP tools for those workflows.

WarmHub does not currently publish an OpenAPI specification. For exact request and response shapes, see the documentation in this section or the SDK reference.

The most common HTTP task is reading a repository’s current state — its HEAD. This is a GET, so it is safe to retry; reads on a public repo also work without a token.

Terminal window
curl -H "Authorization: Bearer <token>" \
"https://api.warmhub.ai/api/repos/warmhub-data/us.congress.trades/head?shape=CongressTrade&kind=thing&limit=1"
{
"items": [
{
"wref": "20034954/bd598743649e738e-2",
"name": "20034954/bd598743649e738e-2",
"kind": "thing",
"active": true,
"version": 1,
"data": { "ticker": "CSCO", "amount_range": "$1,001 - $15,000", "transaction_type": "sale_partial" }
}
],
"nextCursor": "whc1_eyJ2Ijox..."
}

The rest of this page covers the base URL, auth, formats, and retries that apply across endpoints; the routing table points to each endpoint family.

Most REST endpoints use the /api prefix:

https://api.warmhub.ai/api/

The SSE transport is root-mounted, using the origin without /api:

https://api.warmhub.ai/sse

The MCP server has its own hostname — see MCP Server:

https://mcp.warmhub.ai/mcp

For example, the repository HEAD endpoint is:

https://api.warmhub.ai/api/repos/acme/world/head

Authenticated endpoints require an Authorization header with a Bearer token:

Terminal window
curl -H "Authorization: Bearer <token>" \
https://api.warmhub.ai/api/repos/acme/world/head

Repository data read endpoints (/head, /about/:wref, /query) on public repositories can be read anonymously; the same endpoints on private repos require authentication. Action-observability endpoints (/api/repos/:org/:repo/actions/*) always require authentication regardless of repo visibility.

The export endpoint (GET /api/repos/:orgName/:repoName/export) always requires authentication regardless of repo visibility. The token must carry both repo:read and repo:checkpoint-read scopes, and the repo:read grant must be unrestricted (not scoped to a subset of shapes or matches). A token with only repo:checkpoint-read, or with a restricted repo:read grant, will receive a 403.

For POST /mcp, the server accepts two tiers of access and its behavior differs by protocol era:

  • Legacy path — anonymous callers (no token) can reach initialize, tools/list, ping, and a subset of tools/call requests that don’t require a verified identity.
  • Modern 2026-07-28 path — anonymous callers can reach server/discover, tools/list, and a subset of tools/call requests. ping is not handled on this path and returns a -32601 error with HTTP 404.

On both paths, callers with a valid bearer token can reach the full set of tool calls. A presented bearer token that is invalid — expired, revoked, forged, malformed, or associated with a disabled account — is rejected before handler dispatch and returns a 401 challenge marked invalid_token. Only an absent bearer falls through to anonymous-tier access; a bad token does not degrade to anonymous and will not reach any handler, including anonymous-tier methods such as initialize. Tool calls that require a verified identity return a 401 challenge before dispatch if no valid token is present. For a full explanation of which tool calls fall into each tier and how clients discover the requirement, see the MCP server documentation.

PAT management REST endpoints are not currently mounted. Create and manage PATs with the CLI (wh token) or the SDK (client.token.*); see Authentication.

  • Content-Type: application/json for JSON request bodies
  • Path parameters: URL-encoded (for example, 20034954%2Fbd598743649e738e-2 for the Thing name 20034954/bd598743649e738e-2)
  • Query parameters: Standard URL query string format

Repository read and action observability endpoints return JSON on success. The /sse endpoint streams text/event-stream, and MCP endpoints follow JSON-RPC over HTTP.

Export endpoint exception: GET /api/repos/:orgName/:repoName/export does not follow the JSON-only pattern. When the export is ready and streamed synchronously (no token), the response body is application/x-ndjson. When a previously minted export token is redeemed and the export has completed, the response is 200 JSON with metadata — { url, expiresAt, atRepoSeq, recordCount, contentSha256 } — containing a signed URL the client fetches separately to retrieve the export bytes. An async export mint or an in-progress token redemption returns 202 with a JSON body. Raw HTTP callers building against the repository read surface must not assume a uniform content type across all export response paths.

Most REST errors use this envelope:

{
"error": {
"code": "NOT_FOUND",
"message": "Thing not found: temp-1"
}
}

Three error codes carry an additional machine-readable error.details arm that callers can branch on:

VALIDATION_ERROR — emitted for request validation failures. error.details contains structured field-level validation issues. The top-level error.message reflects the first validation issue encountered (for example, "Expected number, received string"), so callers should not match on a fixed message literal; branch on error.code instead.

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Expected number, received string",
"details": {
"reason": "validation_failed",
"issues": [
{ "path": "limit", "message": "Expected number, received string" }
]
}
}
}

issues[].path is a dotted string (e.g. "limit" or "body.name") identifying the field that failed validation.

RATE_LIMITED — error.details contains structured retry metadata. Callers should also inspect the Retry-After response header (see Idempotency and retries).

{
"error": {
"code": "RATE_LIMITED",
"message": "Rate limit exceeded",
"retryAfter": 4,
"details": {
"reason": "rate_limit_reset",
"retryAfterSeconds": 4,
"resetAt": "2024-06-01T00:00:04Z"
}
}
}

The top-level error.retryAfter field (in seconds) is the stable field to read for retry timing; error.details carries the same value alongside reason and resetAt for callers that need the structured form.

UNAUTHENTICATED (presented-but-invalid bearer) — when a bearer token is present but cannot be verified (expired, revoked, forged, malformed, or associated with a disabled account), the response is 401 UNAUTHENTICATED and error.details carries a stable reason discriminator callers can branch on:

{
"error": {
"code": "UNAUTHENTICATED",
"message": "Invalid token",
"details": {
"reason": "invalid_token"
}
}
}

Branch on error.details.reason === "invalid_token" to distinguish a bad credential from other UNAUTHENTICATED responses (such as a missing token on a protected endpoint). Do not match on error.message, which is not a stable contract.

Thing names are unique per repository, whatever Shapes a Thing declares. A write that claims a name another active Thing already holds is refused with 409 CONFLICT when the two share a declared Shape (or neither declares any), and with 409 NAME_TAKEN when their declared Shapes are disjoint or you cannot read the holder. A rename onto a name that an earlier rename in the same commit already claimed also gets 409 NAME_TAKEN. Handle both codes as a name collision. The NAME_TAKEN refusal is terminal, so retrying the same name cannot succeed. error.details carries { "reason": "name_taken", "name", "takenBy": { "durableId" } }. The message names no Shape and the refusal offers no replacement name. Guard the optional member before reading it: takenBy is conditional and may be withheld, while reason and name are present whenever details is — and in rare cases details is omitted altogether. To learn the holder’s Shapes, get the durableId: a Thing can have several Shapes or none.

Other error codes may also populate error.details when the domain error carries structured context; treat it as an optional, code-specific extension. When error.details is absent, the code and message fields are sufficient for error handling.

HTTP StatusCommon Error Codes
400VALIDATION_ERROR, SHAPE_MISMATCH, RESERVED_NAME, ILLEGAL_OP_SEQUENCE
401UNAUTHENTICATED
403FORBIDDEN
404NOT_FOUND
409CONFLICT, NAME_TAKEN, ARCHIVED
412CLIENT_INCOMPATIBLE, PRECONDITION_FAILED
413PAYLOAD_TOO_LARGE
429RATE_LIMITED
500INTERNAL_ERROR

SDK callers receive these as WarmHubError.code / kind. See the ErrorKind reference for retryability and corrective actions per kind.

Repository read endpoints (/head, /about/:wref, /query) collapse missing-repo and unauthorized-private-repo cases into the same 404 so repo existence stays opaque. Action observability endpoints also collapse anonymous, under-scoped, and missing-repo deny paths to opaque 404 responses. See Queries and Actions for the full deny-path behavior.

Early /sse failures use the same standard envelope as other REST errors. A missing ticket parameter returns 400 VALIDATION_ERROR; an invalid ticket returns 401 UNAUTHENTICATED. Both follow the { "error": { "code": "...", "message": "..." } } shape shown above.

When REST endpoints return 429 RATE_LIMITED, they may include a Retry-After response header. Clients should back off until that interval has elapsed.

A 412 means one of two things, and error.code tells you which:

  • CLIENT_INCOMPATIBLE — your client was not accepted. Upgrade it. The SDK and CLI identify themselves for you and check every mutation, so an out-of-date one fails here rather than mid-write. Raw HTTP callers do need to send X-WarmHub-Client to get past this gate — its value is <client-name>/<version>, for example warmhub-cli/1.4.2 — but sending it is not authorization and does not make an unsupported client safe, so prefer a released WarmHub client over hand-rolling the header. (Refused when the header is missing or malformed, when it names a client family the server does not recognize, or when the version is not valid SemVer or is below that family’s floor.)
  • PRECONDITION_FAILED — a component’s credentials are not usable, so the problem is that component’s configuration rather than your request. Credentials can be missing, unreachable from the install’s repo, or carry no auth scheme the caller supports. See the credentials and cli sections of the manifest reference.

A 413 PAYLOAD_TOO_LARGE is returned when a request body exceeds the endpoint’s size limit. The SDK’s write helpers stay under the limit automatically, so you generally only see this on oversized requests built by hand.

GET endpoints — repository reads and action observability — are idempotent. Retrying one returns the same result with no side effects, so they are always safe to retry after a network error.

When a request returns 429 RATE_LIMITED, the response may carry a Retry-After header. The same value is mirrored in the body as error.retryAfter (in seconds). Back off until that interval has elapsed before retrying.

The action callback endpoint, POST /api/action-runs/:runId/callback, is safe to retry with the same status: re-reporting a status the run has already reached succeeds without repeating side effects such as notifications. Reporting a status that conflicts with the run’s current state returns 409 CONFLICT. A retry_requested status is the exception — it is a command, not a report, so each accepted one queues a new delivery attempt and it is rejected once the run’s attempts are exhausted.

WarmHub does not accept an inbound Idempotency-Key header. For action delivery, WarmHub sends your webhook handler an X-WarmHub-Idempotency-Key header so the handler can recognize and discard duplicate deliveries.

Pagination behavior differs by endpoint family:

  • Repository query endpoints (/head, /about/:wref, /query) — accept limit and cursor; return a nextCursor field when more pages are available. Pass nextCursor as cursor on the next request.
  • Export endpoint (/export) — does not use cursor/nextCursor. Resume an interrupted stream using afterDurableId combined with the pinned atRepoSeq from the export manifest. See Queries for the full export pagination details.
  • Action runs (GET /api/repos/:orgName/:repoName/actions/runs) — accept limit, since, and cursor; since and cursor are mutually exclusive and cannot be combined in the same request. Returns a nextCursor field when more pages are available.
  • Action notifications (GET /api/repos/:orgName/:repoName/actions/notifications) — accept limit and since only; returns a plain array with no cursor-based pagination.

See Actions for the per-endpoint details.

ParameterDescription
limitMaximum number of items to return per page.
cursorOpaque pagination token from a previous response’s nextCursor field. Supported on repository query endpoints and the action runs endpoint. Cannot be combined with since on the action runs endpoint.
sinceFilter action records after this epoch-milliseconds timestamp. Cannot be combined with cursor on the action runs endpoint.
afterDurableIdResume an export stream after the last successfully received record. Must be combined with the pinned atRepoSeq. Supported on the export endpoint only.
afterVersionWith afterDurableId, when the last received record was a Shape version: resume with that Shape’s later versions. Supported on the export endpoint only.
atRepoSeqPins the export to a specific repo sequence, ensuring a consistent snapshot across resumed requests. Supported on the export endpoint only.
NeedPage
The complete map of mounted endpoints + per-endpoint authEndpoints
Authenticate requests and manage tokensAuthentication
Read repository data (HEAD, about, query, export)Queries
Action observability and the callback endpointActions
Rate limits and backoff behaviorRate Limiting

Endpoints is the complete map — including the surfaces handled through the SDK, CLI, and MCP rather than REST.