Skip to content

HTTP Endpoints

The endpoints shunt serves as a Claude Code LLM gateway.

Updated View as Markdown
Method Path Purpose
HEAD / Liveness probe
GET / Human-readable landing (version + endpoint list)
GET /health Healthcheck — {"status":"ok","version":"x.y.z"}
GET /v1/models Model discovery — returns your [[models]] entries
GET /routes shunt-native route discovery — returns the configured [[routes]] table verbatim (model → provider/upstream_model/effort mapping, including claude-prefixed discovery aliases); distinct from /v1/models, which serves the narrower Anthropic-protocol discovery response (id, display_name, and upstream model metadata)
POST /v1/messages Inference — routed per the request’s model id
POST /v1/messages/count_tokens Token counting
GET /managed/settings Per-user Claude Code managed settings for a gateway JWT; supports ETag, If-None-Match, and 304 Not Modified
GET /v1/organizations/spend_limits List stored spend caps with directional cursor pagination
POST /v1/organizations/spend_limits Create or replace a spend cap for one (scope, period)
GET /v1/organizations/spend_limits/{id} Fetch one stored spend cap
DELETE /v1/organizations/spend_limits/{id} Delete one stored spend cap
POST /v1/metrics Inbound OTLP/HTTP metrics from managed Claude Code clients — relayed verbatim to opted-in gateway telemetry destinations
POST /v1/logs Inbound OTLP/HTTP log records — relayed only to destinations with logs = true
POST /v1/traces Inbound OTLP/HTTP spans — relayed only to destinations with traces = true
GET /admin Admin dashboard (HTML); redirects to /admin/login when not signed in
GET, POST /admin/login Admin-token login form, optional OIDC affordance, and browser-session creation
POST /admin/oidc/start Start the optional same-origin admin OIDC/PKCE login
GET /admin/oidc/callback Complete OIDC login, enforce the current allowlist, and create the browser session
POST /admin/logout Clear the browser session
GET /admin/accounts Claude account-store metadata: name, kind, expiry, and UUID; never token material
GET /admin/accounts/codex Codex account-store metadata: name, expiry, and ChatGPT account ID; never token material
GET /admin/observed Read-only local Claude Code, Codex, Gemini, Kimi, Grok, and Cursor identity plus provider-native usage; never returns token material or refreshes source credentials. The Claude row also carries the account uuid (null when it cannot be established) so the dashboard can tell an observation and a managed pool account holding the same subscription apart from two different accounts
GET /admin/pool Per-claude_oauth/chatgpt_oauth/kimi_oauth-provider managed-pool state; account objects may include an optional plan string; a file-derived value can later be refined toward a more precise one via a profile lookup; Codex rows include reported 5h/7d usage (7d_oi has no Codex analog); each account also carries a boolean needs_relogin: the credential was terminally rejected (invalid_grant), carries no refresh token at all, or had a rotated pair lost before it could be stored, so no retry can revive it and only an operator re-login will. It is reported independently of the cooldown fields — a cooldown expires by itself, this does not — and both dashboard tables show it as needs re-login rather than the cooling a quota pause produces. Memory-only: a restart clears it, and the account’s next terminal failure re-establishes it. It is reported even for an account no provider table has ever selected — alongside has_state: false — because the admin refresh probe records its verdict by store name.
GET /admin/status Observation-only view of [server.status] polling: each configured source’s most recently observed Statuspage indicator, description, incidents, and observed timestamp. Empty sources when [server.status] is absent or unconfigured — never consulted by routing or failover
POST /admin/accounts/claude Start Claude browser provisioning with {name, mode} where mode is oauth or setup_token (omitted defaults to setup_token); returns {authorize_url}
POST /admin/accounts/claude/{name}/complete Complete Claude provisioning with {code} containing <code>#<state>; stores the account and reports whether it is live
POST /admin/accounts/claude/{name}/refresh Exercise an imported Claude account’s refresh grant on demand and report whether the login is still alive. Rate-limited (it reaches the provider token endpoint), and always routed through the shared credential store so it cannot race the proxy’s own refresh. Returns the new expires_at and never any token material, plus a needs_relogin read back from the pool after the probe’s own clear — a grant can succeed against an account the pool still considers dead, and the response says so rather than claiming a recovery /admin/pool would contradict. Answers 400 for a setup_token account (it carries no refresh grant) or any terminal verdict, and 502 for a non-terminal failure
DELETE /admin/accounts/claude/{name} Remove the named Claude account’s store file
POST /admin/accounts/codex Start ChatGPT OAuth with {name}; returns {authorize_url}
POST /admin/accounts/codex/{name}/complete Complete Codex provisioning with {code} containing the full localhost redirect URL or <code>#<state>; stores the account and reports whether it is live
DELETE /admin/accounts/codex/{name} Remove the named Codex account’s store file
POST /backend-api/codex/responses Inbound Codex CLI passthrough — mirrors the real ChatGPT backend path
POST /responses Inbound Codex CLI passthrough — bare base_url form
POST /v1/responses Inbound Codex CLI passthrough — /v1-suffixed base_url form
POST /backend-api/codex/analytics-events/events Codex CLI analytics sink — accept and discard; record sanitized event-name counters only
POST /codex/analytics-events/events Codex CLI analytics sink — root-style chatgpt_base_url form
GET /usage Client-facing sanitized pool usage — per-window remaining headroom and reset for the shared account pool; never account identity or capacity
GET /api/oauth/usage Claude Code CLI’s own native usage-bar fetch path — sanitized, Claude-only, routing-aware worst-case pool usage in Anthropic’s own wire shape
GET /.well-known/oauth-authorization-server Gateway OAuth discovery metadata
POST /oauth/device_authorization Start a gateway device authorization grant
POST /oauth/token Poll a device grant or refresh a gateway session
GET, POST /device Browser device-code confirmation page and password approval form
POST /device/authorize Start OIDC approval after explicit same-origin browser confirmation
GET /device/callback Complete the OIDC authorization-code callback

The gateway discovery, device, token, and browser routes exist only when [server.gateway] is configured at startup. POST /device/authorize and GET /device/callback additionally require [server.gateway.oidc]; without OIDC configuration they return a browser error page rather than starting an external sign-in. Adding or removing [server.gateway] requires a restart because route registration is fixed at boot, while changes to its resolved credentials and OIDC configuration hot-apply.

The /managed/settings route exists only when [server.gateway] was enabled at boot. A valid gateway bearer JWT is required; static [server.auth] tokens do not authenticate this endpoint. When [[server.gateway.policies]] is configured, the response is:

{
  "uuid": "sha256:<stable-user-hash>",
  "checksum": "sha256:<settings-hash>",
  "settings": { "availableModels": ["claude-opus-4-8"] }
}

ETag is the quoted checksum ("sha256:<settings-hash>"). Send it back in If-None-Match to receive 304 Not Modified with an empty body when settings have not changed; comma-separated validator lists, weak validators, *, and legacy unquoted checksum values are accepted. No configured policies returns 404; a policy that resolves to an empty document returns 200 with settings: {}.

The POST /v1/{metrics,logs,traces} telemetry-ingest routes exist only when [server.gateway] was enabled at boot, and require the same gateway bearer JWT as /managed/settings. They accept the OTLP/HTTP payloads managed Claude Code clients export — [server.gateway.telemetry] points those exporters at the gateway — and relay the exact request bytes to every destination that opted in to the signal, preserving the inbound content-type and content-encoding and applying the destination’s own configured headers over them (a configured key replaces the forwarded value rather than duplicating the header). The client’s Authorization header is never forwarded, and relays do not follow redirects. Destinations opt in per signal (metrics on by default, logs and traces off), and a signal with no opted-in destination is accepted and discarded. Relays are detached, so the response is always an immediate 200 regardless of destination health — the success body mirrors the request protocol per OTLP/HTTP ({} for application/json, an empty application/x-protobuf body otherwise); a body over the 32 MiB inbound cap returns 413.

The spend-limit routes exist only when [server.spend] was configured at boot — independently of [server.gateway], since they authenticate with the [server.admin] credential. Send that credential in the configured admin header (x-shunt-admin-token by default) or in x-api-key; both slots are accepted. A write credential (a write_keys entry, or a tokens_env/tokens_file pair) can use every operation, while a read_keys credential can use GET only and receives 403 on mutations. POST accepts user and organization scopes, a daily/weekly/monthly period, a user_id of 1–256 bytes for user scopes, and an amount that is either a 1–19 digit whole-number string of USD cents or null. It upserts by (scope, period). List pagination accepts limit (1–1000, default 20), after_id, before_id, and scope_type; the two cursors are mutually exclusive. Every response includes request-id, and errors use the Anthropic error shape. Caps and mutation audit records persist together in the configured versioned JSON state file, each mutation attributed to admin-key:<id> or admin-token:<name> — when both slots carry a different credential of the same tier, the configured header is the attributed one. Stage 1 does not expose /effective or /audit and does not enforce caps on inference requests.

The /admin* routes exist only when [server.admin] is configured; without that table, none of them are registered. They accept the admin credential in the configured header or x-api-key, and a read_keys credential passes every GET below while being refused with 403 on every mutation and with 401 on POST /admin/login. GET /admin/observed auto-discovers supported Claude Code, Codex CLI, Gemini CLI, Kimi Code, Grok CLI, and Cursor.app credentials on the gateway host. It never refreshes or writes those sources. Claude usage is cached for 60 seconds; Codex usage is response-derived and remains unavailable until traffic through this shunt returns x-codex-* headers; the other providers use their first-party read-only quota surfaces. Managed account CRUD and /admin/pool remain the separate shunt-owned credential lane.

The /backend-api/codex/responses, /responses, /v1/responses, /backend-api/codex/analytics-events/events, and /codex/analytics-events/events routes exist only when [server.codex_endpoint] is configured; without that table, none of them are registered. The three Responses paths relay raw OpenAI Responses requests and responses, unlike the Anthropic-Messages-translating /v1/messages above. The two analytics paths use the same inbound-auth policy, never forward or retain the client payload, and return 200 {} after authentication even for malformed or oversized bodies. Only sanitized event names are counted in shunt.codex_client_events; with no metric sink configured they are pure discard sinks. See the inbound Codex endpoint guide.

The /usage route exists only when [server.usage] is configured, which itself requires [server.auth]. It authenticates the same client token as GET /v1/messages (configured header, x-api-key, or Authorization: Bearer) and returns a sanitized, aggregated view of the shared account pool — per-window remaining headroom and reset time plus a coarse ok/degraded/exhausted status — so a non-admin caller can anticipate throttling. It never exposes account names, counts, priorities, disabled flags, thresholds, or per-account numbers; the full per-account detail stays behind admin-only GET /admin/pool. A window is null only when no non-disabled account reports it. Codex response x-codex-* headers and optional wham/usage polling populate the observed 5-hour and shared weekly windows; an unobserved window alone is null. Codex has no Fable-scoped (7d_oi) signal, although another provider in a mixed pool may supply the aggregate Fable window. Response shape:

{
  "pool": {
    "status": "ok",
    "windows": {
      "5h":    { "remaining": 0.42, "resets_at": 1752000000 },
      "7d":    { "remaining": 0.61, "resets_at": 1752500000 },
      "fable": { "remaining": null, "resets_at": null }
    }
  }
}

The /api/oauth/usage route exists only when [server.oauth_usage] is configured. It is the exact path the Claude Code CLI’s own usage bars fetch (fetchUtilization), so when the CLI is pointed at shunt via ANTHROPIC_BASE_URL, its unmodified UI can show real numbers — but only for CLIs using a full interactive claude login session; claude setup-token and shared-gateway client-token setups were verified not to trigger the CLI’s own fetch (see the M14 behavior specification for the full precondition evidence). Unlike GET /usage, auth is bind-topology-gated: unauthenticated on a loopback [server] bind, and requiring a valid credential — a configured client token or a valid gateway JWT, exactly as /v1/messages is gated — on a non-loopback bind (which itself then requires [server.auth] or [server.gateway] to be configured). Bare header presence is not accepted. It reports only claude_oauth-provider accounts, using a routing-aware, priority-tiered worst case per window rather than /usage’s pool-wide least-utilized aggregate — the worst case among the accounts the next request can actually route to, not an optimistic pool-wide minimum. It never exposes account names, counts, priorities, disabled flags, thresholds, or per-account numbers. Response shape (Anthropic’s own /api/oauth/usage schema, resets_at in RFC3339):

{
  "five_hour": { "utilization": 42.37, "resets_at": "2026-07-20T23:00:00Z" },
  "seven_day": { "utilization": 61.02, "resets_at": "2026-07-27T00:00:00Z" },
  "limits": [
    {
      "kind": "weekly_scoped",
      "scope": { "model": { "display_name": "Fable" } },
      "percent": 12.5,
      "resets_at": "2026-07-27T00:00:00Z"
    }
  ]
}

five_hour/seven_day are omitted (not null) when no non-disabled Claude account reports that window; limits is omitted entirely (not an empty array) when no account reports the Fable-scoped window.

GET / and GET /health stay open even when [server.auth] is enabled (healthcheck tools usually cannot attach tokens) and expose nothing sensitive — only status, version, and the already-public endpoint list. With [server.auth] enabled, GET /v1/models requires a valid client token in the configured header, x-api-key, or Authorization: Bearer; it stays open when inbound auth is not configured. GET /routes remains open as shunt-native routing metadata.

Gateway protocol

shunt implements the official Claude Code LLM gateway protocol: correct header and body-field forwarding, feature pass-through, and system-prompt attribution handling. Gateway-owned errors are returned in the Anthropic error shape, upstream context-overflow errors are rewritten to Anthropic’s prompt is too long wording so Claude Code’s compact-and-retry fires, and streaming responses are relayed without buffering (with optional keepalive pings).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close