Endpoint reference
Every route on the gateway: /v1/, /mcp, /scim/v2/, /admin/, /connect/.
Auth lanes
| Surface | Gated by |
|---|---|
/v1/* | An enrolled client key (minted by anyray-connect) or a non-human service key. Attribution rides x-anyray-metadata. |
/mcp/admin | An AuthKit bearer for exactly this gateway's /mcp/admin URL, resolved to the live console grant. |
/mcp/org | The caller's client key, or a WorkOS AuthKit token whose audience is this gateway's exact /mcp/org URL. |
/admin/* | The admin token (ANYRAY_ADMIN_TOKEN), a role-scoped console SSO session, or a scoped admin API key (aak_…, see the API reference). |
/scim/v2/* | Its own static bearer, set through PUT /admin/scim/settings. Not an admin-console session. |
Authenticated is not privileged. Every /admin/* route also needs the capability in its row
below, per the RBAC lattice
(viewer → auditor → operator → security_admin → owner).
In fleet mode (ANYRAY_MULTI_TENANT=true) most configuration routes also need the deployment
owner (a tenant SSO principal gets 403 whatever its role). Tenant-scoped reads and client test
publishing stay role-gated only: /admin/client-tests, /admin/spend*, /admin/health, /admin/me,
/admin/client-keys, /admin/service-keys, and the optimizer and observability routes. Rows below mark the deployment-owner routes where the
distinction most often bites.
Inference requests and the VS Code pre-call hook carry content under the deployment's content mode. SCIM identities, admin spend records, and request logs never expose prompt or response bodies.
Capabilities from the OpenAPI spec
Every /admin/* operation in the OpenAPI spec carries
x-required-capability, the one capability that route enforces (null means authenticated but
ungated, like /admin/me). It is generated from the gateway's route table, and CI fails on drift.
Use it to script an access matrix, or to pick the smallest grant for a
scoped admin API key:
# Which capabilities does a script touching these routes need?
curl -s https://docs.anyray.ai/openapi.json \
| jq -r '.paths | to_entries[] | .key as $p | .value | to_entries[]
| select(.value["x-required-capability"])
| "\(.key | ascii_upcase) \($p) -> \(.value["x-required-capability"])"'
Management API v1: /admin/v1/*
Stable The /admin/* surface below, re-served under resource paths with
a scope you derive from the URL: GET needs <resource>:read, anything else <resource>:write. The
named exceptions are in the API reference.
Each v1 operation is the same handler as its legacy twin, so the request and response bodies
documented in the legacy rows apply unchanged. Only the path, the gate, and the error envelope
differ. The spec carries x-required-scope, x-scope (tenant or deployment), and
x-legacy-operation on every v1 operation.
| Resource | Paths | Scopes | Legacy twins |
|---|---|---|---|
me | GET /admin/v1/me | none | /admin/me |
me/logout | POST /admin/v1/me/logout | none; console session required | native v1; bearer logout |
keys | /admin/v1/keys (?type=service; ?limit=&cursor= for {data,next_cursor} pages), …/{id} (GET/PATCH/DELETE), …/{id}/rotate, …/{id}/secret | keys:read, keys:write, keys:secret | /admin/client-keys, /admin/service-keys |
admin-keys | /admin/v1/admin-keys, …/{id} | admin-keys:read, admin-keys:write | /admin/api-keys |
users | /admin/v1/users/limits, …/{user}/limits, …/auth-events, …/revoked, …/revoked/{email}, …/{user}/export, …/{user}/data | users:read, users:write, users:revoke, users:gdpr | /admin/user-caps, /admin/auth-events, /admin/revoked-users, /admin/gdpr/users/… |
spend | /admin/v1/spend/summary, …/dashboard, …/overview, …/active-users, …/activity, …/usage, …/usage/properties, …/sessions/{sessionId}, …/quality-parity, …/external | spend:read | /admin/spend, /admin/spend/*, /admin/spend-connectors/usage |
client-tests | /admin/v1/client-tests (GET/POST) | client-tests:read, client-tests:write | /admin/client-tests |
provider-resources | /admin/v1/provider-resources (GET), …/{provider}/{id} (GET), …/{provider}/{id}/owner (PUT/DELETE) | provider-resources:read, provider-resources:write (providerresources:manage; security_admin and owner) | /admin/provider-resources |
providers | /admin/v1/providers, …/capabilities, …/{slug}/key (PUT/DELETE), …/connectors, …/connectors/sync, …/web-search (GET/PUT/DELETE) | providers:read, providers:write | /admin/provider-keys, /admin/spend-connectors, /admin/claude-desktop-policy/web-search |
routing | /admin/v1/routing, …/references, …/aliases, …/agent-models, …/models | routing:read, routing:write | /admin/routing-config, /admin/model-aliases, /admin/agent-models, /admin/pricing |
optimizer | /admin/v1/optimizer, GET /admin/v1/optimizer/log-read, …/purge, …/erase-tenant, GET /admin/v1/optimizer/session-gate, PUT /admin/v1/optimizer/session-gate/{kind}, GET /admin/v1/optimizer/tenant-policy, PUT /admin/v1/optimizer/tenant-policy/{tool}/{version} | optimizer:read, optimizer:write, optimizer:erase | /admin/optimizer/settings, GET /admin/log-read/status, /admin/optimizer/purge, /admin/optimizer/erase-tenant, GET /admin/optimizer/session-gate, PUT /admin/optimizer/session-gate/{kind}, GET /admin/optimizer/tenant-policy, PUT /admin/optimizer/tenant-policy/{tool}/{version} |
policies | /admin/v1/policies/teams, …/claude-desktop, …/claude-desktop/mobileconfig, …/claude-desktop/connectors/{probe,readiness,authorize,timing} | policies:read, policies:write | /admin/team-skills, /admin/claude-desktop-policy, …/connectors/* |
settings | /admin/v1/settings, …/content, …/updates, …/updates/run, …/setup, …/setup/status, …/prompt-cache, …/health, …/health/series, …/health/problems, …/support-bundle, …/support-bundle/share | settings:read, settings:write, settings:content, settings:update | /admin/settings, /admin/settings/prompt-cache, /admin/update-status, /admin/update/settings, /admin/update/run, /admin/wizard-*, /admin/health, /admin/support/bundle |
sso | /admin/v1/sso, …/scim, …/portal-link, …/members, …/members/{email} | sso:read, sso:write | /admin/idp-config, /admin/scim/settings, /admin/workos-portal-link, /admin/console-members |
enrollment | /admin/v1/enrollment/links, …/links/{id} (?hard=true) | enrollment:read, enrollment:write | /admin/enrollment-links |
provisioning | /admin/v1/provisioning/tokens, …/{id}, …/{id}/rotate | provisioning:read, provisioning:write | /admin/provisioning-tokens |
fleet | /admin/v1/fleet/seats, …/directives | fleet:read, fleet:direct | /admin/connect-health, /admin/connect/directives. …/seats is the per-seat connect-health rollup. appliedIntegrationLevel is present only when Connect reports a recognized level. residue is three-state: omitted = not reported, [] = probed clean, non-empty closed list = reported leftovers; only an actionable member (any except the informational osquery_log_retained) derives state: "needs_pkg_install". The /admin/v1/fleet/seats twin returns the identical body. |
traces | /admin/v1/traces (next_cursor pages), …/exists, …/facets, …/sessions, …/{id}, …/{id}/content, …/export, …/connector-credential-scrub | traces:read, traces:content, traces:scrub | /admin/observability/*, /admin/traces/connector-credential-scrub. traces:read lets a caller call these; observability:read decides whether they answer for the whole organization or only the caller's own traffic |
audit | /admin/v1/audit/events ({data,next_cursor} pages), …/metering | audit:read | /admin/metering/last-report |
playground | POST /admin/v1/playground | playground:run | /admin/test-request |
Two reads are sharper than their verb suggests. GET /admin/v1/providers needs providers:write,
because provider keys come back in full; …/capabilities is the credential-free view on
providers:read. GET /admin/v1/traces/{id} is metadata-only for every caller. …/{id}/content
and …/export (a time window as NDJSON, no legacy
twin) are the only trace paths that return bodies, and only to a principal holding traces:content
(never a key).
Not re-served: the console session routes (/admin/auth, /admin/auth/update, /admin/sso/*) and
the legacy /admin/spend/connectors alias.
GET /admin/v1/settings/setup/status reports setup progress in a fixed step order: provider_keys,
routing, email_domain, team_link, first_request. The console's setup guide and Get started card read it too.
done,optionalandpassthroughsettle a step.todo,pending,missing_keys,no_targetandwaitingneed action.missing_keysnames routed providers with no key, or pinned to a deleted key.no_targetmeans a routing group has no target a request can be sent to.unreadable,not_connectedandnot_permittedmean the gateway could not check the step. They never count as settled.completeis true when every step is settled.nextnames the first step that needs action, or is null.- Every unsettled step carries
action: anoperationIdto call (or null) and a shorthint. first_requestlooks back 90 days, the same window the console's Home uses to decide whether to show setup. It ignores the console playground's test requests.
The response holds no credentials, DNS challenges or developer identities.
Inference API: /v1/*
OpenAI-compatible, plus the Anthropic-native Messages API. Point any OpenAI or Anthropic SDK's base URL at the gateway. Full schemas: the OpenAPI spec and the API reference.
| Method | Path | What it does |
|---|---|---|
| post | /v1/chat/completions | Chat completions, streaming and non-streaming. |
| post | /v1/messages | Anthropic-native Messages API. |
| post | /v1/messages/count_tokens | Token counting. No inference, not metered. |
| post | /v1/messages/batches | Anthropic Message Batches (GET list; GET /v1/messages/batches/{id}, POST …/{id}/cancel, GET …/{id}/results). Requests ride inline, so no /v1/files upload step. Metered content-free, one spend row per result line. |
| post | /v1/responses | Responses API create (GET/DELETE /v1/responses/{id}, GET …/{id}/input_items). A GET with Upgrade: websocket is Codex's Responses WebSocket on a ChatGPT seat, relayed unchanged; anything else it cannot serve answers 426, including a cap, budget or entitlement refusal, which reaches Codex as 402 or 403 on the HTTPS request it sends instead. |
| post | /v1/completions | Legacy text completions. |
| post | /v1/embeddings | Embeddings. |
| get | /v1/models | List available models. A route to Bedrock, which has no list to forward to, is answered from the gateway's own catalog. |
| get | /v1/me · /v1/me/usage · /v1/me/limits · /v1/me/models | Self-service for the verified key holder: identity and key metadata, month-to-date tokens and savings, the caps that would 402 this identity, and the selectable models. Answered by the gateway, metadata only. |
| post | /v1/audio/speech | Text to speech. |
| post | /v1/audio/transcriptions · /v1/audio/translations | Audio transcription and translation. |
| post | /v1/images/generations · /v1/images/edits | Image generation and edits. |
| post | /v1/files | Upload a file (GET list; GET/DELETE /v1/files/{id}, GET …/{id}/content). |
| post | /v1/batches | Create a batch (GET list; GET /v1/batches/{id}, POST …/{id}/cancel). |
| get | /v1/realtime | Realtime WebSocket passthrough. |
| post | /v1/extensions/vscode/optimize | Optimize a VS Code extension request before the extension calls its provider. Fails open; the optimizer's savings are metered as a zero-token spend row. |
| post | /v1/copilot/:plan/:seat/* | GitHub Copilot seat carrier lane (GET too). The gateway forwards the seat token, so GitHub still bills the Copilot plan. |
| post | /v1/seat/cursor/chat/completions | Cursor OpenAI-BYOK lane. Configured by anyray-connect, not called by hand. |
A /v1/ path the gateway has no route for is forwarded to the provider for GET, DELETE, file
and audio uploads, bodyless POSTs, and requests that send x-anyray-auth-mode: passthrough. Any
other JSON POST to one answers 404 with a hint naming the main inference routes, so a wrong base
URL fails at once instead of being retried.
On a provider key the gateway holds, some of those paths answer 400 instead:
| Provider | Not forwarded on the org's key |
|---|---|
vertex-ai, bedrock | Every unrouted path, including /v1/proxy/*. GET /v1/models still answers, unless the request carries an x-anyray-config or x-anyray-custom-host. |
Every other provider except google | Every unrouted path except GET model listing (anything under /models) and the provider's inference paths that have no route here. The routes in the table above, such as /v1/files and /v1/batches, are not affected. |
openai | The admin API, /v1/organization/*, on any route |
anthropic | The admin API, /v1/organizations/*, on any route |
Of those inference paths, only a body that is not application/json reaches the provider: a JSON POST
to an unrouted path is a 404 before any of this, so moderations, rerank and OCR, which take JSON, do not
forward. Image variations, a multipart upload, does.
A caller's own credential (pass-through, or a key sent with no key stored) is forwarded as before. The admin API check judges the path past the base URL. An admin's custom host is that base, so its own path may name an organization. A custom host the caller names is judged whole.
/v1/me/models lists aliases, the models routing lanes name, and the priced ids a $regex lane
matches. When nothing names the default provider, its priced family is listed where routing reaches
that provider. anyray-default is the last row. Rows carry id and known token limits. A person with a route or a service
key gets its own menu instead, the one the admin route below returns for that key. An
agent-binding or agent-lanes menu has no anyray-default.
The provider and a per-key menu come from GET /admin/v1/routing/agent-models?keyId=<key record id>
(routing:read; legacy GET /admin/agent-models on config:read). Its scope says which menu
you got: agent-binding (the key has its own route, so its providers' priced models),
agent-lanes (only the models of that key's own lanes) or org (the menu above). Without keyId
it is always org. With keyId, primary names the key's default model: the menu id it sent the
most successful requests for in the last 30 days (callers with spend:read), else the first model
its own lanes name exactly. It is absent when neither applies. An unknown keyId returns 404 with
error.code: key_not_found, and a key-store failure returns 503.
max_tokens clamping on the Anthropic lanes
On /v1/messages, and on /v1/chat/completions with an Anthropic provider, a max_tokens above
the model's hard output ceiling is clamped down to that ceiling instead of being rejected upstream
with a 400. A tool that sets one provider-wide value for every model (OpenClaw's per-provider
maxTokens, say) still succeeds at the largest output the model supports. Values at or under the
ceiling, unknown model ids, and subscription passthrough traffic pass through unchanged. The value
is never raised.
What the Cursor seat lane rewrites
/v1/seat/cursor/chat/completions is served by the /v1/chat/completions pipeline but classified
as an Anyray subscription seat, because Cursor cannot send a seat header. Before validation it
folds Cursor's wire shape into a chat body: a Responses-API envelope (input, instructions, flat
function tools, max_output_tokens, with store, previous_response_id and truncation
dropped), flat tools on a chat body, and a missing model filled with anyray-default. The reply
is the provider's chat-completions response. This is API-key-upstream traffic, not the native
Cursor Team entitlement.
Files, batches and stored responses on the org's key
On a provider key the gateway holds, a file, batch, fine-tuning job or stored response belongs to the
person who created it. Only they can read, download, cancel or delete it. Anyone else gets 404, the
same answer as an id that does not exist. A batch's output and error files, and a fine-tuning job's
result files, belong to the owner of the batch or job.
List calls return only your own, so a page can hold fewer items than you asked for. Page on with
after set to the page's last_id. A page that holds none of yours has no last_id, so paging stops
there; narrow the list instead (for files, by purpose). A Responses request with store: false keeps
nothing at the provider, so it has no owner.
A request that names a file by id is checked the same way: a file_id in a chat, Messages or
Responses request (tool results included), a batch's input_file_id, a fine-tuning job's training and
validation files, and a Responses previous_response_id. Naming one you don't own answers 404.
A batch's input file is checked too. When you upload a file with purpose=batch on the org's key,
the gateway reads each line as it passes through. The upload fails before the file is created if a
line names a file or stored response you don't own (404), or isn't a JSON object, or is longer than
50 MB (400). One input file can name up to 1 MB of distinct ids; more answers 400. A batch on the org's key takes
only an input file uploaded this way. Any other file, including one uploaded before this check, one an
admin assigned, or one with another purpose, answers 400: upload it again with purpose=batch. So
does an input file an admin transferred to you, since its lines were checked against its previous
owner's files. The gateway checks the ids an input names again when you create the batch, so a batch
whose input names a file that has since moved to someone else answers 404.
A method these routes don't serve, such as DELETE on a fine-tuning job, answers 405 on the org's
key, and nothing reaches the provider.
Creating a file, batch or fine-tuning job on the org's key needs a verified identity: a client key
or SSO identity. Without one, the create answers 403 and nothing reaches the provider.
| Who sends the request | Who owns what it creates |
|---|---|
| A client key | The person the key is enrolled to. |
| A service key | The key itself. The user it names in x-anyray-metadata is attributed spend, not ownership, because the gateway can't verify that header. |
A request with no key, under identityMode: network-trusted | Nothing: its file, batch and fine-tuning creates answer 403, because the user it names is not verified. A stored Response it creates has no owner and stays admin-only. |
While the gateway can't reach its ownership store, these routes answer 503, and a create is not
sent to the provider. The store is the spend database, so a gateway without one, including one on
Workers, answers 503 here on the org's key. Your own provider credential (pass-through) is not
checked; the provider's own account rules apply.
If something you just created answers 404, the gateway could not save its owner. An admin can
assign it to you with the transfer below.
Erasing a user releases what they owned, so it becomes admin-only, and a GDPR export lists the files, batches, jobs and stored responses a user owns (ids and dates only).
Files, batches, jobs and stored responses created before the gateway checked ownership have no owner, so only an admin can reach them. That includes a batch still running. An admin can hand one back with a transfer. Transfer each one to the person who created it, not to yourself: a batch's output spend is booked to whoever downloads it.
To transfer a resource, an admin with provider-resources:write sets its owner. From a terminal
with the admin CLI:
anyray-connect admin api GET provider-resources --query provider=openai --query kind=batch --json
printf '%s\n' '{"owner":"jane@example.com","kind":"batch"}' |
anyray-connect admin api PUT provider-resources/openai/batch_abc123/owner --body - --json
From the admin MCP, call anyray_admin_write with v1_put_provider_resources_provider_id_owner.
DELETE …/owner releases a resource, leaving it admin-only. A batch's or job's output files move with
it on a transfer or release, unless they were assigned to someone else. A file an admin assigns is
never marked as a checked batch input, so to use it as one, upload it again with purpose=batch. Every
transfer and release is audited. These routes read and write ownership records only. They never call the provider and never
return a file's or response's content.
Inbound SCIM v2: /scim/v2/*
Okta, Microsoft Entra ID, and other SCIM clients push verified users and groups here. Every request
sends the SCIM bearer as Authorization: Bearer <token>. Responses use application/scim+json, and
errors use the SCIM 2.0 error envelope.
| Method | Path | What it does |
|---|---|---|
| get | /scim/v2/ServiceProviderConfig | Advertise PATCH, filtering, paging, and bearer authentication. |
| get | /scim/v2/ResourceTypes | List the User and Group resource types. |
| get | /scim/v2/Schemas | Return the supported core User and Group schemas. |
| post | /scim/v2/Users | Create a user from the verified IdP payload. |
| get | /scim/v2/Users | List users. Supports filter=userName eq "dev@example.com", startIndex, count. |
| get | /scim/v2/Users/:id | Read one user. |
| put | /scim/v2/Users/:id | Replace a user. |
| patch | /scim/v2/Users/:id | Patch activation, display name, or identity type. active:false blocks the user's client keys. |
| delete | /scim/v2/Users/:id | Soft-delete and deactivate a user, so existing client keys stay blocked. |
| post | /scim/v2/Groups | Create a group and resolve or create its members. |
| get | /scim/v2/Groups | List groups with startIndex and count paging. |
| get | /scim/v2/Groups/:id | Read one group and its members. |
| put | /scim/v2/Groups/:id | Replace a group and recompute member team and role mappings. |
| patch | /scim/v2/Groups/:id | Add, remove, or replace members; update the display name. |
| delete | /scim/v2/Groups/:id | Delete a group and recompute affected identities. |
Billing App machine API
Served by the Billing App (app.anyray.ai), not the gateway. The gateway calls it with its adt_
deployment token. Usage rollups, lease state, release metadata, or an explicitly submitted redacted
support bundle. Never prompt or response content.
| Method | Path | What it does |
|---|---|---|
| post | /v1/meter | Ingest a usage rollup plus deployment heartbeat. Returns a freshly signed entitlement lease. |
| get | /v1/entitlement | Lease heartbeat: refresh the signed lease without ingesting usage. ?deploymentId= binds it like a meter report. Throttled on its own clock; 429 carries Retry-After. |
| post | /v1/tenant-offboard/ack | Acknowledge a signed offboarding notice delivered on a deleted deployment's metering 401. |
| get | /v1/tenant-context | Fleet mode only: resolve an ark_ key hash to its tenant context. |
| get | /v1/releases/latest | The current release-line version and its bounded requirements manifest. manifest may be null. |
| post | /v1/support-bundles | Accept a reviewed, redacted support bundle, analyze it, and return { receiptId }. Deployment bearer, 1,000,000-byte cap, one per deployment per minute. |
| post | /v1/connect-events | Gateway-down fallback for connect-health. Authenticated by a DevCert and proof of possession, not a deployment bearer. Best-effort, throttled, never on the billing path. |
| post | /v1/cert/renew | Roll a live DevCert forward, so a machine in use never has to re-enroll. Authenticated by a signed challenge. Refused for a revoked developer or deployment, and for an SSO-issued certificate whose tenant SSO policy is absent or unreadable. |
| post | /v1/cli-sso/start | Start the identity half of a CLI login. Takes the gateway session id and user code, returns the browser activation URL. |
| get | /v1/cli-sso/poll | Return pending, or the verified identity, governed team list and default, resolved role, and configured key lifetime. Never returns the gateway key. |
| put | /v1/enrollment-identities | Record a deployment-verified roster identity with allowlisted source and type. Used by gateway SCIM reflection. |
CLI self-service login
Pre-key gateway routes used only by anyray-connect login. Start is rate-limited by source IP. Poll
is authorized by the separate high-entropy secret start returns once; the secret travels in a header,
not the URL, and the gateway stores only a salted hash.
| Method | Path | What it does |
|---|---|---|
| post | /sso/cli/start | Create a durable ten-minute login session, start the Billing App SSO grant, and return { id, browser_url, user_code, poll_secret, interval, expires_in }. |
| get | /sso/cli/poll/:id | With x-anyray-poll-secret, return pending, the eligible team list, or the one-time minted key. Send the selected team in x-anyray-team. |
The Billing App also serves the browser-only /sso/cli/activate/:id, /sso/cli/authorize, and
/sso/cli/callback pages. They are HTML and redirect surfaces, not integration APIs.
Desktop sign-in
Public Billing App routes used by the Connect desktop app before it belongs to a deployment. Start
returns a human code separately from the browser URL and registers an exact, randomized
http://127.0.0.1:<port>/<43-character-base64url-token> callback with no query or fragment. The
Billing App stores hashes, not the completion or poll secrets.
| Method | Path | What it does |
|---|---|---|
| post | /sso/desktop/start | Accept { devPublicKey, callback_uri }, bind the Ed25519 key and loopback callback, and return { id, browser_url, user_code, poll_secret, interval, expires_in }. The user_code is not part of browser_url. |
| get | /sso/desktop/poll/:id | With the poll secret, callback-delivered completion secret, and start-key Ed25519 proof in x-anyray-poll-secret, x-anyray-completion-secret, and x-anyray-key-proof, return the signed DevCert with its deployment and gateway URL. Replayable until the ten-minute grant expires. |
/sso/desktop/activate/:id only shows a code form. The browser posts the code to
/sso/desktop/authorize, and one correct live code redirects to AuthKit. The callback maps the
verified email domain to one live deployment and redirects to the registered listener with grant_id
plus completion_secret, error=access_denied, error=access_removed (the email is on the tenant's revoked list), or error=no_company. Missing or invalid poll key
proof gets the same 404 as an unknown grant. A poll for a person revoked after approval returns
200 { status: "rejected", reason: "access_removed" } and no certificate, even on a retry; the canonical signed message is in
billing/PROTOCOL.md. Transient poll 503 responses carry retryable: true; configuration failures
do not.
Health & settings
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | / | None | Readiness. 200 normally, 503 once graceful shutdown starts, and 503 while a metered gateway holds no entitlement lease at all (an expired or suspended lease stays 200). Point your readiness check here. |
| get | /livez | None | Liveness. 200 for as long as the process can serve, including while draining, and checks no dependency. Point your liveness check here, never at /. |
| get | /docs | None | The interactive API explorer over this gateway's own spec. Shows only the API shape. |
| get | /openapi.json | None | The OpenAPI 3.1 spec of /v1/*, /admin/*, and /connect/*. Load it into Postman or a generator. |
| get | /admin/health | config:read | Liveness, spend-store ping, schema version (applied vs expected) with jsonbChecks column-constraint counts, optimizer reachability, pools connection budget and built-pool counters, the portal leg with leaseSource, leaseTrust, leaseAcquire, and clientIpShadow: this replica's counts for the client-IP rule that is measured but not enforced (Gateway TLS). It carries branch (trust_proxy_off, rightmost_public, railway_first_entry), railwayVariable, per-limiter seen and differs, and collapse: over the last 10 minutes, the most of today's keys that one new key would merge (maxLegacyKeysPerKey) and how many new keys merge more than 1, 5 and 20. No addresses. egressProxy says whether a corporate forward proxy is configured: configured, the proxy host:port per scheme (https, http), and noProxyEntries, never credentials (Behind a forward proxy). clientKeySnapshot describes this replica's in-memory copy of live (not revoked, not expired) client keys and the SCIM roster that /v1 verifies against: state (off, loading, fresh, or stale once the last successful load is over 30 s old, when verification reads the database per request), ageMs, keyRows, rosterRows, and lastRefreshMs, the last load's query time. Each replicas[] entry carries the same block as of that replica's last publish. |
| get | /admin/health/series | config:read + deployment owner | Read health slots for the last 24 hours, open problems and the newest 50 history rows. Optional from and to (epoch milliseconds) pick another range within the last 7 days; a range over 24 hours returns hourly points. Cached for 60 seconds per range. No live probes run on this route. |
| get | /admin/health/problems | config:read + deployment owner | Read 50 problem log rows. For the next page, pass the last row's since, service, id and replicaId as query parameters. |
| get | /admin/me | None | The caller's identity plus deploymentOwner, false for tenant SSO principals in fleet mode. |
| get | /admin/settings | config:read | Read Postgres-backed runtime settings, including mcpConnectTool (default false) and mcpConnectorSignInTools (default true). |
| put | /admin/settings | content:manage | Update runtime settings: contentMode, heartbeatTier, storeCapacityBytes, mcpConnectTool, mcpConnectorSignInTools, promptCacheTtl, promptCacheKeepWarm, promptCacheKeepWarmIdleMaxMinutes, promptCacheKeepWarmSubscription, promptCacheRecordedExpiry, hookStash, hookAmortizationTurns, codexSeatAppsDefer, claudeCodeRemoteMcp, personalUpstream, claudeDesktopRetrievalMcp, toolDefer, ccBudgetNotice, artifactsEnabled, artifactsOrgShare, harnessRetrieval (and, deprecated for one release, promptCache1h and promptCacheKeepWarmSubscriptionIdle). See the notes below. |
| get | /admin/settings/prompt-cache | config:read | The prompt cache settings (ttl, keepWarm) and the 1 hour recommendation's state (on, recommended, off, not_enough_data, unavailable), with the last 7 days of session evidence behind it, the keep-warm returnHazard in use (the built-in prior and this deployment's observed pauses, per stop reason), and what auto picks and why. Deployment owner only. |
| put | /admin/settings/identity | identity:manage | Set how /v1/* establishes who is calling. See the warning below. Audited. |
| get | /admin/metering/last-report | config:read | The exact final payload from this process's last successful verified usage report, plus heartbeat tier and metering state. |
| get | /admin/update-status | config:read | Self-updater status: available vs running version, the local preflight verdict (missing environment-variable names plus breaking notes), the computed updateClass (soft or hard), the autoSoftUpdates and autoFallback policies, hasApplier, and the last manual or automatic attempt (updateAttempt, including attemptId; autoUpdate remains a compatibility alias), shared by every gateway replica (autoUpdateSource is shared, replica without a spend database, or unavailable when it cannot be read). autoUpdateLoop is this replica's last tick: its outcome (triggered, up-to-date, rejected, store-unavailable, ...) and the store it ran under. store-unavailable is one skipped tick; the next one falls back to the replica's own last attempt, so an unreachable spend database delays an update by one tick instead of stopping it. rejected and unreachable mean the applier itself failed and are reported until the retry window passes. |
| put | /admin/update/settings | update:run | {"autoSoftUpdates": true|false, "autoFallback": true|false}, at least one: whether image-only updates apply unattended, and whether Connect switches to direct when the gateway is down. Both default on. Platforms without an applier are notify-only. |
| get | /admin/auth | None | Auth-mode probe (pre-session; reports whether SSO is required). |
| post | /admin/update/run | update:run + deployment owner | Twin of POST /admin/v1/settings/updates/run (settings:update, unscopeable). Dispatch a newer manifest-confirmed soft target through the existing HTTP applier, even with auto-update off. No overrides. 202 returns target, attemptId and attempt metadata, never installation confirmation. Shared pending claim before dispatch; six-hour duplicate window; ten-second bound. 409 for hard/unknown preflight, 501 update_applier_unavailable with platform guidance, 503 for unavailable shared storage, 502 for a refused dispatch. Cross-site requests are refused. Poll the GET's running version and check fleet convergence. |
| get | /admin/auth/update | update:run + deployment owner | Compatibility capability probe: 200 only when the pending update is soft, else 403. The gateway POST enforces its own checks. |
/livez, not at // answers "should traffic be routed here?" and turns 503 the moment graceful shutdown begins. A
liveness probe aimed at it restarts the container mid-drain and severs every in-flight streaming
response, which client tools report as "Connection closed mid-response" against the gateway.
/livez answers "should this process be killed?" and checks no dependency on purpose: a liveness
probe that tested Postgres would turn a brief database blip into a fleet-wide crashloop. For
dependency health use /admin/health, a diagnostic rather than a probe. Both exist from
v1.10.224, and the Helm chart wires them automatically.
identityMode: network-trusted removes the key requirementPUT /admin/settings/identity takes {"identityMode": "enrolled"|"network-trusted"}. enrolled is
the default and requires every request to present a minted client key. network-trusted lifts that
requirement for a deployment that is already network-isolated, attributing spend from the request
instead. It also turns off SCIM offboarding enforcement and leaves the server-held provider keys
reachable by anything on that network. A key-less request counts toward no person's
cap or budget or subscription allowance.
A key that is presented is still verified and still binds an authoritative identity in either
mode; only the absence of one is forgiven. Single-tenant self-hosted only: a fleet-mode deployment
resolves identity through the control plane, which never consults this setting, so /v1/* there
still returns 401 missing_key without a valid tenant key. The separate identity:manage capability
is one no scoped admin API key may ever hold.
PUT /admin/settings and the console are the only ways to set the content mode. There is no
environment variable for it. storeCapacityBytes declares the durable store's real volume capacity in
bytes (minimum 1 GiB; null clears it), so storage-runway alerting measures against that ceiling.
Set it on any deployment whose Postgres runs in its own container or service. Without a ceiling
there is nothing to measure the store against, so the runway reads as unknown instead of being
guessed at. Give it the size the volume can grow to, such as an RDS storage-autoscaling maximum. It
needs settings:content on /admin/v1, and it is a declaration, not a measurement. The console
sets it on the Privacy page (Database storage, in GiB). Set
{"mcpConnectTool": true} to replace the per-connector tools mcpConnectorSignInTools serves by
default with one anyray_connect covering every waiting connector, including a row the gateway has
not classified.
seatOverflow and seatOverflowHourlyMaxRequests are retired: PUT /admin/settings answers 400
retired_setting for either. Set seat overflow with the
subscription fallback policy,
fallback.org on PUT /admin/team-skills.
Spend & governance
The spend surface and per-user governance: token caps and USD budgets.
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/spend | config:read | In-memory spend summary: requests and tokens per attributed user. |
| get | /admin/spend/dashboard | config:read | Windowed dashboard: money by billing class, trend, per-model/team/user/client splits, top strategies, latency, success rate, prompt-cache economics. Includes kpis, the four headline tiles with their daily series. ?window=. |
| get | /admin/spend/overview | config:read | Money by billing class, daily trend, and model/team/user/client splits, including attributed savings-only models and saved tokens by model for each user. Omits latency, cache, retrieval, and strategy diagnostics. Includes kpis and holdout (the audited holdout's cost-per-session reading for the window). ?window=; optional ?from=/?to= (inclusive UTC days, YYYY-MM-DD) narrow every aggregate except holdout and echo back as range. |
| get | /admin/spend/active-users | config:read | All attributed users with billed tokens or saved tokens in the window, as users: string[]. ?window=. |
| get | /admin/spend/activity | config:read | AI activity per person, or per agent with ?by=agent: activity spans (requests 5 minutes apart or closer join one span), request counts, and each person's reported time zone. ?window= (7d, 30d or 90d, with the same UTC-day bounds as /admin/spend/dashboard), or ?from=/?to= (ISO instants, a range of up to 91 days starting no more than 91 days ago); a range of 6 hours or less adds requests per minute. ?user= returns one person. Agent requests, keep-warm pings and service accounts never count toward a person. |
| get | /admin/spend/usage | config:read | Per-user usage detail: per-model and per-day breakdown, org per-model rollup, cap gauges. ?window=, ?limit=; optional ?from=/?to= (inclusive UTC days, YYYY-MM-DD) narrow every aggregate and echo back as range. |
| get | /admin/spend/quality-parity | config:read | Holdout vs treated quality parity, with tenant and pairedSession fields and measured gross-cost reduction against the 30% objective, with 95% CIs. One anyray-bench request excludes its whole tenant-scoped session from both arms and the paired-session gate. benchTraffic counts every request in those sessions, plus kit requests without a session id, and reports requests, sessions and cost. ?window=, ?cohortKind= (a strategy id selects that strategy's arm, log_read the test-log Read lane's; omitted reads the whole-optimizer arm). |
| post | /admin/client-tests | clienttests:write | Publish a content-free client test result. Returns 201 {result} with a server id and receive time. Strictly accepts hashes, versions, token-shaped ids, counts, ratios and verdict enums. |
| get | /admin/client-tests | config:read | List results newest first as {results}, ties by id descending. ?limit= is an integer from 1 to 200, default 50. ?latest=plan returns each plan's newest result, with the limit counting plans. Other latest values return 400. Tenant-bound callers see their tenant only. The v1 twin accepts the same queries. |
| get | /admin/spend/usage/properties | config:read | Spend grouped by one bounded custom attribution property (?name=, ?window=): values, counts, tokens, and USD only. The raw attribution map is never returned. |
| get | /admin/spend/sessions/:sessionId | config:read | One client session's spend (the Claude Code session id), split by requestKind: null for requests the client sent, cache_warm for keep-warm pings the gateway sent for it. Requests, cost, savings, and prompt, completion, and cache tokens. hookTrims counts the session's Connect hook trims (events, tokensSaved, grossSavingsUsd); batchReads.avoidedTurns counts the model turns batch reads saved. ?includeSubagents=true adds the session's subagent rows. Unknown ids read as zeros. |
| get | /admin/auth-events | config:read | Reason-coded auth and cap rejections with remediation hints. ?user=, ?limit=. |
| get | /admin/user-caps | config:read | Per-user token caps and USD budgets, current-month token usage, per-user budgetUsage, and cap-alert posture. The webhook URL is never returned. |
| put | /admin/user-caps | usercaps:write | Replace the whole caps map, plus alert thresholds and webhook. Returns the GET shape. |
| patch | /admin/user-caps/:user | usercaps:write | Merge a partial edit into one user's entry (omit = keep, null = clear). Never touches alert settings. Returns the GET shape. |
| get | /admin/spend-connectors | config:read + deployment owner | Read redacted Cursor, Devin, and GitHub Copilot connector state and poll status. Credentials are never returned. |
| put | /admin/spend-connectors | providerkeys:manage + deployment owner | Partially set, rotate, or remove write-only connector credentials. Omitted connectors stay unchanged; null or {} removes one. {connector, token} replaces only the credential, keeps enabled as stored, and keeps Copilot scope and slug unless sent. A 409 names its cause in error.code: connector_credentials_unreadable (stored credentials no longer decrypt, as after an admin-token rotation) or secret_sealing_unavailable (no private admin token). |
| post | /admin/spend-connectors/sync | providerkeys:manage + deployment owner | Sync vendor usage. Returns safe per-connector counts, or skipped/failed. |
| get | /admin/spend-connectors/usage | config:read + deployment owner | Aggregate vendor-native usage. ?days= accepts 1 to 90, default 30. |
clienttests:write is an operator capability that scoped admin keys may carry. On v1, publishing
needs client-tests:write; listing needs client-tests:read (implied by config:read). Neither
route needs deployment-owner access. An invalid body returns 400 with issue paths and codes only;
an unavailable result store returns 503. An empty store returns {results: []}.
A scenario may include twinOf, the client plan hash its public twin represents: 64 lowercase
hex characters, excluding all zeros. Omit it when there is no client plan. The gateway stores
and returns it verbatim; invalid values return 400 at scenarios[i].twinOf.
Each scenario may include predictedHitRate: a number from 0 to 1 for its band's hit rate in
earlier tests, or null when there are fewer than 8 calibration rounds. Omit it for an unrated band.
Each scenario may include parity, the paired check outcomes: n graded pairs, passDirect,
passAnyray, anyrayWorse (pairs only the direct arm passed) and anyrayBetter (pairs only the
Anyray arm passed). All are integers from 0 to 10000. The counts must reconcile, so passAnyray equals
passDirect - anyrayWorse + anyrayBetter, and neither pass count nor the two pair counts together
may exceed n. A mismatch returns 400 at scenarios[i].parity. Omit it from older kits.
A result may include evaluatedKinds, the optimizer kinds its test sessions applied: on (on for
the tenant) and shadow (still in shadow, so live traffic does not get them yet). Both are integers
from 0 to 10000, and no other key is accepted. An invalid value returns 400 at evaluatedKinds.
Omit it from older kits; a missing field means not reported, not zero.
Spend connector rules
Connector credentials (cursor.adminApiKey, devin.serviceUserToken, githubCopilot.token) are
write-only. Responses show configuration and poll status, safe counts, and GitHub's non-secret
scope and slug. They never return credentials, vendor payloads, signed URLs, or vendor errors.
POST /admin/spend-connectors/sync returns {connectors: [...]}. Each connector reports fetched,
inserted, and duplicate counts, {skipped: true} when another replica is syncing, or
{failed: true} without the vendor error. Usage keeps each vendor's units separate, and Copilot's
public list value is an estimate, not an invoice total.
These routes manage the whole deployment, so in fleet mode tenant SSO users cannot use them even
with the listed capability. Use the deployment owner credential or an unbound local owner session.
The deprecated /admin/spend/connectors path uses the same encrypted store for older clients; new
clients should use /admin/spend-connectors.
Per-user caps and budgets
Each entry may carry a monthly token cap (monthlyTokens), a hard USD ceiling (maxUsd), an
alert-only USD threshold (softUsd), and the window the dollar counters roll on (reset: never,
daily, weekly, or monthly, all UTC, weeks starting Monday). Every field is optional.
put /admin/user-caps replaces the whole map, so read the current config
first and send it back with your edit. Send the revision you got from GET back as
expectedRevision and the write becomes conditional: if anything changed in between, the gateway
answers 409 and applies nothing.
{
"caps": {
"dev-synthetic-1": { "monthlyTokens": 5000000 },
"dev-synthetic-2": { "maxUsd": 200, "softUsd": 150, "reset": "monthly" }
},
"alerts": { "thresholds": [0.8, 1] },
"expectedRevision": "9f2c1ab47e05d3purelysynthetic0000"
}
{
"config": {
"caps": {
"dev-synthetic-1": { "monthlyTokens": 5000000 },
"dev-synthetic-2": { "maxUsd": 200, "softUsd": 150, "reset": "monthly" }
},
"alerts": { "thresholds": [0.8, 1], "webhookConfigured": false },
"revision": "9f2c1ab47e05d3purelysynthetic0000"
},
"usage": { "dev-synthetic-1": 1284000 },
"period": "2030-06",
"budgetUsage": {
"dev-synthetic-2": { "spentUsd": 164.2, "period": "2030-06" }
}
}
For a single user prefer patch /admin/user-caps/:user. It edits one entry
without re-sending the map, retries a lost race internally when expectedRevision is omitted, and
cannot touch the alert settings. It is the lane built for
scoped admin API keys; the minimal grant for a budget bot
is ["config:read", "usercaps:write"].
Both verbs answer with the same shape, and never the alert webhook URL (only webhookConfigured).
Users with no dollar budget are absent from budgetUsage.
Both limits are enforced on the inference endpoints and reject with 402
(user_token_cap_exceeded / user_budget_exceeded). Three things to know about the dollar figure:
- It is real out-of-pocket spend, not list price. An included subscription seat accrues only its
over-allowance overage, and spend equal to
maxUsdcounts as over. maxUsdonly blocks API-key traffic. Subscription seat lanes (Claude Code, Codex, Copilot, Cursor) pass even over budget and notify instead.- Enforcement is soft and fails open. Unattributed requests, unset limits, an unwarmed config, and a spend-store outage all pass through, and because the counter moves after a response completes, in-flight requests can overshoot.
Crossing softUsd or maxUsd posts one payload to the configured cap-alert webhook and emails your
workspace's billing contact, once per user, threshold, and window across the whole fleet.
Optimizer, pricing & routing
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/optimizer/settings | config:read | Read the optimizer pipeline config. |
| put | /admin/optimizer/settings | optimizer:write | Update the optimizer pipeline config. clientStrategies.log_read is not stored in it: it reads and writes the team policy's org-level hooks.logRead, and a change needs teamskills:write and the deployment owner. |
| get | /admin/log-read/status | config:read | The test-log Read lane (log_read) in production: fleet (seats seen in 14 days: active, off by policy by level, held back by the regret guard, not available, not reporting), guard (the lane's regret guard: state, window re-read rate, trip point) and holdout (whether the Measurement card is measuring it, the paired-session reading of cost per session for its own arm, and replica-local shadow counters). Counts only. |
| get | /admin/optimizer/session-gate | config:read | Per-tenant strategy states and last paired-session evaluation. |
| put | /admin/optimizer/session-gate/{kind} | optimizer:write | Set or clear an operator gate override. tenant_policy__<tool> is one client tool's learned-policy unit and tenant_policy the tenant-wide one: force_off prices that tool's (or every) session on the defaults, and force_on is refused. |
| get | /admin/optimizer/session-gate/{kind}/sessions | config:read | One gate unit's sessions over the last days (1 to 30, default 7), at most limit (default 2000, cap 20000). Each session is its arm and arm-blind facts: tool set, entry size, cost, turns, output tokens, error and worst flags. No user, no session id. Also returns the gate's own cost statistics over every session read, and the unit's last 20 recorded looks. policy selects a policy key (default: the unit's current one). |
| get | /admin/optimizer/tenant-policy | config:read | Learned policy versions per client tool with their state (shadow, measured, active, rejected, retired), and the gate units judging them: tenant_policy__<tool> per tool, and the tenant-wide tenant_policy. learner gives the learner's latest outcome per step: each tool's fit verdict (published, no_gain, no_holdout, thin, no_sessions, no_trace_store, already_fitted, error) with its sample sizes, and the fleet-wide tick and lock_busy. |
| put | /admin/optimizer/tenant-policy/{tool}/{version} | optimizer:write | Move a version of one client tool's line: measured starts its trial (one per tool at a time), rejected stops it, retired turns the active version off, active rolls a retired version back. Only the session gate makes a measured version active. A heavy user's own line (<tool>.u-<digest>) is trialled and judged on its own, under its own gate unit. |
| post | /admin/optimizer/purge | optimizer:purge | Purge optimizer caches and session state. |
| post | /admin/optimizer/erase-tenant | optimizer:purge | Offboarding: erase one tenant's context stash now. erased: true only after the delete commits; 503 otherwise. Data boundary. |
| get | /admin/pricing | config:read | The read-only official price table. |
| get | /admin/agent-models | config:read | The model menu with each row's provider and token limits, never the upstream model. ?keyId=<key record id> returns that key's menu and its scope. |
| get | /admin/model-aliases | config:read | Read model-alias mappings. |
| put | /admin/model-aliases | modelaliases:write | Replace model-alias mappings. Send { aliases, revision } or use If-Match to refuse a stale save with 409. A bare map contains only aliases, including an entry named revision. |
| get | /admin/routing-config | config:read | Read routing (single / loadbalance / fallback / conditional). |
| put | /admin/routing-config | routing:write | Update routing config. |
| get | /admin/v1/routing/served?user={id} or ?agentId={id} | routing:read | Last 20 inference attempts, newest first; person reads cover the last 30 days. Returns { served: [{ at, model, provider?, providerKeyId?, billingMode?, status }] }. Exactly one id; malformed ids return 400 invalid_request. Org-wide reads also need traces:read (scope-minted keys) or observability:read. Other readers see only their own identities and assigned agent keys. A person's list excludes requests signed by agent keys and asserted identities. |
| get | /admin/v1/routing/users | routing:read | List person routes with user, revision, and readable. Readable rows include route. |
| get | /admin/v1/routing/users/{user} | routing:read | Read a person route. Unreadable rows return readable: false without route. 404 not_found when absent. |
| put | /admin/v1/routing/users/{user} | routing:write | Send { route, expectedRevision? }. A stale revision returns 409 conflict. |
| delete | /admin/v1/routing/users/{user} | routing:write | Delete a person route. 404 not_found when absent. |
| post | /admin/test-request | playground:run | Console playground: send a test request through the gateway. |
Person routes hold a required default, optional rules (at most 16), and optional
providerKeys. Destinations are subscription, org, or an agent routing document.
Each rule matches one exact model or a known model family. User ids fold ASCII case.
Changing or clearing providerKeys also needs providerkeys:manage (providers:write
for API keys). A target's providerKeyId needs only routing:write.
API-key requests apply these routes, including model rules and provider-key maps.
Routing precedence and subscription fallback.
Routing reads (GET /admin/routing-config and GET /admin/v1/routing) mask inline provider
credentials as {"$anyrayCredentialRef":"<64 lowercase hex HMAC>"}. The response also includes
inlineCredentials: {present, targets}, a presence flag and the number of nodes holding inline
credentials. Stored config and request dispatch stay unchanged. Guardrail check credentials (fields
under a check's credentials, a sink's headers, every URL a check sends data to such as
webhookURL, a top-level apiKey, and any address that embeds userinfo, a query or a fragment) are
masked the same way. When the store cannot be read, a read serves the gateway's cached copy and
says so with fromCache: true; reload before acting on it.
Keep these objects unchanged when saving. A successful PUT replies with the config as stored, read
like a GET, so that reply saves back unchanged. If that read fails, the save still happened and
the reply is 503 routing_config_saved_unreadable; reload before saving again. A reference resolves
against the authoritative stored config, in the same credential field and with the same provider,
host/base URL, custom endpoints, region and resource addressing. This includes targets inheriting
the credential. An absent or empty root api_key can also be represented by a reference; saving
restores its original state. That root object makes previous releases reject a projected config
rather than store references as credentials.
Add new credentials in Provider keys, then select provider_key_id.
Routing saves return 400 inline_credentials_refused for new literal credentials,
400 routing_sealed_value_refused for a sealed value (a string starting rsec:v<n>:, or an
anyray_sealed object; only the gateway writes those),
400 routing_credential_reference_invalid for unknown or stale references, and
400 routing_credential_address_changed when a reference would send credentials somewhere new.
A guardrail reference stays on its unchanged check, in the same guardrail field and spelling (same
plugin, same parameters in the same key order, except is_enabled); otherwise the save returns
400 guardrail_credential_check_changed. A reference where the read put none returns
400 routing_credential_reference_invalid.
Schema and depth checks run first. Revisions are opaque keyed tokens: send expectedRevision
unchanged. A 409 routing_config_conflict, including across mixed releases, means reload and
reapply the change.
Claude Desktop organization policy
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/claude-desktop-policy | config:read + deployment owner | Read the secret-free organization policy and resolved deployment fields. Personal OAuth grants are not enumerated. An absent row reads as a disabled empty policy. builtinConnectors lists each github, microsoft365 and websearch row of an array-form managedMcpServers by its index, with status served or withheld and, when withheld, a reason: missing_client_id, missing_tenant_id, web_search_not_configured or web_search_unreadable. |
| put | /admin/claude-desktop-policy | desktopconfig:write + deployment owner | Replace {enabled, settings}. Accepts recognized Claude managed keys for models, connectors, plugins and skills, workspace and tool policy, egress, and application restrictions. Rejects server-owned inference and bootstrap fields, credential-shaped string values at every depth, headers and env, nested secret keys, unknown keys, and documents over 128 KiB. A refusal answers 400 with error.code device_profile_only_key, server_owned_key or credential_key (each with error.key), or credential_value or unknown_key. |
| get | /admin/claude-desktop-policy/mobileconfig | config:read + deployment owner | Download the stable macOS trust and helper profile. Needs an enabled policy and a public HTTPS ANYRAY_GATEWAY_PUBLIC_URL. The artifact pins the bootstrap URL (?os=macos) and the bootstrap-headers helper path, and contains no credential. |
| get | /admin/claude-desktop-plugins | config:read + deployment owner | List the organization's Skills, Guidance and plugins. v1 twin GET /admin/v1/desktop-plugins (desktop-plugins:read). |
| put | /admin/claude-desktop-plugins | desktopconfig:write + deployment owner | Save one item, kind skill, guidance or plugin. A plugin, or a save that replaces one, also needs orgplugins:manage (403 without it, and always 403 for an API key). 400 names the broken rule, 409 a skill id another item already holds. v1 twin PUT /admin/v1/desktop-plugins (desktop-plugins:write). See Plugins. |
| delete | /admin/claude-desktop-plugins/:id | desktopconfig:write + deployment owner | Remove one item. Removing a plugin also needs orgplugins:manage. v1 twin DELETE /admin/v1/desktop-plugins/:id. |
| get | /admin/org-plugins/install-states | orgplugins:manage + deployment owner | Counts of reporting seats by tool, plugin id, version and install state over the last 72 hours. v1 twin GET /admin/v1/desktop-plugins/install-states. Admin API keys cannot read it. Response. |
| post | /admin/claude-desktop-plugins/git/inspect, /git/preview, /git/import | orgplugins:manage + deployment owner | Inspect a public GitHub repository, preview one plugin, and import it with its source recorded. Public repositories only, no credentials sent. v1 twins under /admin/v1/desktop-plugins/git/. Admin API keys cannot call them. See Plugins. |
| get | /admin/claude-desktop-plugins/:id/source | orgplugins:manage + deployment owner | Check whether an imported plugin has a newer commit (one request, no download). v1 twin GET /admin/v1/desktop-plugins/:id/source. |
| post | /admin/claude-desktop-plugins/:id/source/preview | orgplugins:manage + deployment owner | Preview what an update would change. Nothing applies until the import endpoint is called with the previewed commit. v1 twin POST /admin/v1/desktop-plugins/:id/source/preview. |
| get | /admin/claude-desktop-policy/marketplaces | config:read + deployment owner | List the marketplaces seats can add, anyray-org-skills first and read-only. v1 twin GET /admin/v1/desktop-plugins/marketplaces. |
| post | /admin/claude-desktop-policy/marketplaces (also DELETE) | orgplugins:manage + deployment owner | Add a marketplace by public GitHub repository (optional ref) or by bare HTTPS URL of its marketplace.json, or remove it. Writes only allowedPluginMarketplaces. Admin API keys cannot change marketplaces. v1 twins on /admin/v1/desktop-plugins/marketplaces. |
| post | /admin/claude-desktop-policy/marketplaces/query | config:read + deployment owner | Fetch one configured marketplace again and return its plugins, or its unreachable, malformed or unusable state with the reason and plugins: null. v1 twin under /admin/v1/desktop-plugins/marketplaces/query. |
| post | /admin/claude-desktop-policy/connectors/probe | desktopconfig:write + deployment owner | Ask the MCP server at {url} for its name and tool list, so a connector can be added from its address. Answers ok (with name, version, tools, truncated), needs-auth, or unreachable with a reason. HTTPS only, nothing stored. |
| get | /admin/claude-desktop-policy/connectors/readiness | config:read + deployment owner | The most-blocking organization sign-in state, /mcp/org endpoint URL, static served and dropped counts, cached access classification, and privacy-floored adoption. Never probes connector vendors. |
| post | /admin/claude-desktop-policy/connectors/readiness/refresh | desktopconfig:write + deployment owner | Refresh the process-local connector access cache with at most four probes in flight, then return readiness. |
| get | /admin/claude-desktop-policy/connectors/timing | config:read + deployment owner | Relay latency per leg (oauthFetchTools, oauthAuthorize, refreshTools, relay): sample count, p50, p95, and the last outcome. Samples live in memory on the replica that answers, so the response names it and a fleet's other replicas hold their own; capped marks a leg whose window is full. No connector URL or vendor body. |
| post | /admin/claude-desktop-policy/connectors/authorize | desktopconfig:write + deployment owner | Start a personal OAuth flow. The result holds only the vendor authorizationUrl; after consent the grant belongs to the verified client-key user who redeems the short claim code. |
| get | /admin/claude-desktop-policy/web-search | config:read + deployment owner | {configured, provider, updatedAt, sealingAvailable} for the Web search key. Never the key. v1 twin /admin/v1/providers/web-search (providers:read). |
| put | /admin/claude-desktop-policy/web-search | providerkeys:manage + deployment owner | Store {provider, apiKey}: provider is brave, tavily or exa, apiKey 8 to 512 characters. Send both every time. The key is encrypted at rest and never returned or audit-logged. 409 when ANYRAY_ADMIN_TOKEN is unset or the public placeholder. v1 twin providers:write. |
| delete | /admin/claude-desktop-policy/web-search | providerkeys:manage + deployment owner | Remove the Web search key. The policy then leaves gateway-served Web search rows out. v1 twin providers:write. |
| get | /connect/claude-desktop/bootstrap | Optional client key | Secret-free effective policy fetched by managed Claude Desktop clients. ?os=macos|windows|linux (default macos) selects the inferenceCredentialHelper path the response names; an unknown value is 400. A presented Authorization: Bearer ark_… is verified (401 when rejected), and no bearer is still served. 404 while disabled. |
| post | /connect/claude-desktop/websearch | Client key | The address the policy's Web search row names. Takes {"q": "..."} (1 to 400 characters), searches with the stored key, and answers {"results": [{"title", "url", "snippet"}]}, at most ten. 401 without a live key, 429 past 30 searches a minute per key, 503 web_search_not_configured or web_search_unavailable, 502 web_search_upstream_failed. Queries and results are never logged or stored. |
Current Claude Desktop (1.34493.1 and later) authenticates the bootstrap fetch through the MDM-pinned
bootstrapHeadersHelper, which Anyray's wrapper fills with the enrolled user's client key, and
requires the response to name inferenceCredentialHelper. The route still serves an unauthenticated
fetch so same-origin managed clients work without a customer OIDC issuer, so treat connector URLs and
restrictions as publicly readable. Credentials and OAuth grants are never accepted into the stored
document. The authenticated admin routes stay available under --headless, so operators can always
inspect, disable, replace, or export a policy that the bootstrap route serves.
Field detail and worked examples: API reference.
Providers
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/provider-keys | providerkeys:manage | List configured providers and named-key metadata (capabilities.keyIds). unreadable: true means this gateway cannot decrypt the stored keys, so config is empty (provider keys). |
| get | /admin/provider-capabilities | config:read | The same list without credentials, so a role that may route but not read a key can still fill its pickers. capabilities.personalUpstream is true on a gateway with the personal upstream setting. |
| put | /admin/provider-keys | providerkeys:manage | Set server-held provider API keys (per-slug patch; per-key form for named keys; fields patches for changed fields). Returns {status, capabilities} with provider slugs and key ids only, never credential values. On a store this gateway cannot decrypt, a full re-entry answers 409 provider_store_unreadable unless sent with ?replaceUnreadableStore=true. |
| get | /admin/provider-resources | providerresources:manage | Who owns the files, batches, fine-tuning jobs and stored responses created on the org's provider keys, oldest first: {data, next_cursor}. Each row has provider, kind, resourceId, owner, parentId and createdAt. Filter with ?provider=, ?kind= (file, batch, fine_tune, response) and ?owner=; page with ?limit= (1 to 500, default 50) and ?cursor=. Ownership records only: never a provider call or content. |
| get | /admin/provider-resources/:provider/:id | providerresources:manage | One resource's owner as {resource}, with owner: null once released, or 404 when the gateway has never recorded it. |
| put | /admin/provider-resources/:provider/:id/owner | providerresources:manage | Set the owner, {"owner": "<user>", "kind": "<kind>"}: transfer a resource, or give one created before ownership checks its creator back. Audited. 400 for an owner that is not a specific user, such as unknown. |
| delete | /admin/provider-resources/:provider/:id/owner | providerresources:manage | Release the resource, {released}. It stays admin-only until it is assigned again. Audited. |
Access, SSO & enrollment
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/idp-config | config:read | Read the SSO / IdP configuration. |
| put | /admin/idp-config | idp:manage | Configure SSO / IdP. |
| delete | /admin/idp-config | idp:manage | Remove the SSO / IdP configuration. |
| get | /admin/revoked-users | config:read + deployment owner | List revoked user emails. |
| post | /admin/revoked-users | idp:manage + deployment owner | Revoke a user: kills their client keys and blocks re-enrollment. The offboarding step. |
| delete | /admin/revoked-users/:email | idp:manage + deployment owner | Un-revoke, allowing re-enrollment. |
| get | /admin/console-members | config:read + deployment owner | List who may sign in to the console by email, and each one's role. |
| put | /admin/console-members | idp:manage + deployment owner | Add an email or change its role, up to operator. You can only grant a role whose capabilities you hold. |
| delete | /admin/console-members/:email | idp:manage + deployment owner | Remove an email. Their current session ends within 12 hours. |
| get | /admin/connect/directives | observability:read + deployment owner | Directive history, every status including acked, newest first. ?user=<userHash>, ?limit=. |
| post | /admin/connect/directives | idp:manage + deployment owner | Queue a per-user connect remediation directive. See the kinds below. |
| get | /admin/scim/settings | config:read + deployment owner | Read redacted SCIM state, admin group, and group-to-team mappings. The bearer and its hash are never returned. |
| put | /admin/scim/settings | idp:manage + deployment owner | Set or rotate the SCIM bearer and update group mappings. Omitting bearerToken preserves it. |
| post | /admin/workos-portal-link | idp:manage | Mint a WorkOS admin-portal link. |
| get | /admin/sso/start · /admin/sso/callback | None | Begin the console sign-in handshake, and its redirect callback (pre-session). Uses the org's SSO when set up, and an emailed code otherwise. /admin/sso/start is limited per IP at the unauthenticated rate. |
| post | /admin/sso/logout | None | End the console SSO session. |
| get | /admin/client-keys | config:read | List minted client keys (enrolled users). A row whose user had a Claude subscription token refused carries seatRejections. |
| post | /admin/client-keys | clientkeys:manage | Mint a client key. |
| delete | /admin/client-keys/:id | clientkeys:manage | Revoke a client key. |
| get | /admin/service-keys | config:read | List non-human service keys. Tokens and hashes are never returned. |
| post | /admin/service-keys | clientkeys:manage | Mint an ark_svc_… key for an AI agent, CI job, or SDK script. |
| delete | /admin/service-keys/:id | clientkeys:manage | Revoke a service key. |
| get | /admin/api-keys | config:read + deployment owner | List scoped admin API keys (aak_…). Metadata only. |
| post | /admin/api-keys | adminkeys:manage + deployment owner | Mint a scoped admin API key with exactly one grant shape. Scope-minted keys are v1-only. A key cannot mint keys. |
| delete | /admin/api-keys/:id | adminkeys:manage + deployment owner | Revoke a scoped admin API key. A key cannot revoke keys. |
| get | /admin/enrollment-links | config:read | List enrollment / setup links. |
| post | /admin/enrollment-links | enrollment:manage | Create an enrollment link with install.sh and install.powershell commands for the default tool choices. |
| delete | /admin/enrollment-links/:id | enrollment:manage | Disable an enrollment link (…/:id/hard hard-deletes it). |
| get | /admin/provisioning-tokens | config:read | List deployment provisioning tokens. |
| post | /admin/provisioning-tokens | provisioning:manage | Mint a provisioning token. kind: "mdm", identityMode: "email", and proofMode: "macos-bootstrap" return the macOS certificate profile once in mobileconfig; list responses never return it. |
| delete | /admin/provisioning-tokens/:id | provisioning:manage | Revoke a provisioning token. |
| post | /admin/provisioning-tokens/:id/rotate | provisioning:manage | Rotate a provisioning token. A macOS bootstrap rotation returns a new one-time profile and certificate fingerprint. |
Directive kind is reapply_config, remint, revert, revert_tool, or update. The machine
pulls it through /connect/directive, within about two minutes on the desktop app and within 30
minutes on CLI-only machines. Delivery is pull-only and never pushed. Only revert_tool takes a
tool (a lowercase adapter slug): it un-routes that tool and leaves the seat enrolled for the rest.
update runs the verified Connect self-updater on supported CLI runtimes only, and the desktop poll
excludes it. See update limitations.
Service key request and response
post /admin/service-keys accepts name and team, each 1 to 64 characters
of letters, digits, dot, colon, hyphen, or underscore (the name feeds the ANYRAY_SEAT_EXCLUDE glob
matcher, so spaces and * are rejected), plus an optional non-negative monthlyBudgetUsd, an
optional delegatedAttribution boolean (see
AI agent enrollment), and an optional telemetryOnly boolean
(an ingest-only key for the Claude telemetry receiver:
/otel/v1/* accepts it and every inference route answers 401 key_telemetry_only). name
becomes the key label and attributed service identity.
{
"name": "synthetic-release-agent",
"team": "platform",
"monthlyBudgetUsd": 50
}
{
"key": "ark_svc_synthetic-once-only",
"record": {
"id": "synthetic-key-id",
"user": "synthetic-release-agent",
"team": "platform",
"label": "synthetic-release-agent",
"tenant": "default",
"createdAt": "2030-06-01T00:00:00.000Z",
"keyType": "service",
"monthlyBudgetUsd": 50,
"source": "service",
"type": "service"
}
}
The 201 returns the raw ark_svc_… value exactly once. The raw key cannot be recovered after
minting; revoke it and mint a replacement. DELETE /admin/service-keys/:id returns { "ok": true }.
Once the key has signed a verified request, the record also carries firstSeenAt, lastSeenAt, and
lastSeenTool: an inferred client id (codex, claude-code, shell-env, unknown) or the tool
the client sent in x-anyray-metadata (openclaw), never the raw User-Agent. A key with none of the
three has not signed a request since these fields shipped, which is how the console's Agents card
tells "never connected" from "active". The same fields appear on GET /admin/v1/keys.
When the last request came from Claude Code, the record also carries lastSeenEntrypoint: cli (a
terminal session or a headless claude -p), sdk (the Claude Agent SDK), ide, or other. It is
overwritten by every request, so a key that moved to another tool has none, and a gateway that predates
it omits it.
The budget covers the key's successful request cost in the current UTC calendar month. At or above
it, inference returns 402 with error.code: "service_key_budget_exceeded". Enforcement is a soft
cap: counters update after the response, so concurrent requests can overshoot, and counter failures
pass traffic through. Omitting the budget, or setting it to 0, leaves the key unlimited.
Privacy, support & observability
| Method | Path | Capability | What it does |
|---|---|---|---|
| get | /admin/gdpr/users/:user/export | gdpr:manage | Export a user's spend records, and the provider files, batches, jobs and stored responses they own (providerResources: provider, kind, id and creation time). |
| delete | /admin/gdpr/users/:user | gdpr:manage | Erase a user's spend rows and cap counters, client keys, context-stash rows, traces and ownership records. Files, batches, jobs and stored responses they created on the org's keys become admin-only. An owner-level request also erases their connector observations and team-policy overrides; agents the user owns are unassigned and keep running. |
| get | /admin/support/bundle | config:read | Generate a support bundle for remote debugging. |
| post | /admin/support/bundle/share | content:manage | Rebuild, redact, and send a support bundle to the Billing App. Returns its receipt and audit metadata. |
| get | /admin/observability/traces/:id | observability:read | Fetch one trace. Content is redacted unless the caller holds observability:read-content. A caller holding only observability:read-own sees their own traces and their agents'; anything else answers 404. |
| get | /admin/observability/* | observability:read | Proxy to the observability read API: lists (traces pages as /admin/v1/traces does), sessions, traces/exists (whether any trace is visible to the caller), and traces/facets (the distinct model, provider and client ids behind the Sessions filters; pass fromTimestamp, toTimestamp or sessionId to read only that slice). |
| get | /admin/artifacts | observability:read-own | Published pages shared with the signed-in person (by name, through their IdP team, or with the whole organization while artifactsOrgShare is on), newest first, with titles and no bodies: { data, next_cursor, skipped } (?limit=, ?cursor=). skipped counts pages in the window that could not be read; next_cursor still continues after them. Only a person's console session reads it: the admin token, a console session signed in with it, and API keys get 403 artifacts_person_session_required. A person the IdP roster deactivated gets 403 artifacts_person_deactivated. Works with publishing turned off. 409 artifacts_unavailable (no_store, no_content_key) when pages cannot be read, and 503 artifacts_read_failed while the store or the IdP roster cannot be read. A malformed cursor answers 400. On every artifact route, an SSO session another organization minted gets 403 artifacts_foreign_tenant, and SSO sessions get 503 artifacts_lease_unavailable while the gateway holds no lease. |
| get | /admin/artifacts/all | observability:read | Every published page in the organization as metadata only (owner, who it is shared with, size, created), never a title or body. Pages like /admin/artifacts. A scope-minted key needs artifacts:metadata on /admin/v1/artifacts/all. |
| get | /admin/artifacts/:id | observability:read-own | Open one page shared with the signed-in person: { data } with its title and body, sent Cache-Control: no-store. A page that does not exist or is not shared with them answers 404. An open by anyone but the author is audited. Refusals match /admin/artifacts. |
| delete | /admin/artifacts/:id | content:manage | Remove anyone's published page. 204, or 404 when there is none. An author removes their own with DELETE /connect/artifacts/{id}. On a multi-tenant gateway the admin token, which names no tenant, gets 409 artifacts_tenant_required here and on /admin/artifacts/all. |
| post | /admin/traces/connector-credential-scrub | content:manage + deployment owner | Find connector credentials that older gateways stored in trace content and list the exposed client keys by id. A dry run unless the body is {"apply": true}, which rewrites those rows redacted. Stops after 20 seconds; send back the returned cursor to continue. 409 while another apply runs. Never rotates a key. Data boundary. |
| get | /admin/connect-health | observability:read + deployment owner | Per-seat connect-health rollup over the last 24h. ?limit= caps seats (1 to 1000, default 200); ?user= narrows to one userHash; ?shape= (repeatable, up to 32 command shapes such as grep or git show) adds those families to hookArrivalShadow. See below. |
| get | /admin/team-skills | config:read | Read shared skills, tool and update policy, eight resolved org hooks.* switches, and stored hooks.teams/hooks.users overrides. An unreadable store reports all switches off and no maps. |
| put | /admin/team-skills | teamskills:write | Replace shared skills and edit hook switches. hooks.digest and hooks.logRead default on; readTrim, lifecycle, prompt, digestEconomics, rangedArrival, and fleetReadBack default off. Omitted hook switches keep their stored values. hooks.ghRunLog and hooks.shellRewrite are retired: a write may still send them, and they are ignored. An explicit hooks.teams or hooks.users map replaces that map; {} clears it. claudeCodeTools.denyFirstPartyOnly and claudeCodeTools.unpinToolSearch (on or off, default off) opt Bedrock-routed Claude Code seats into the two claude-code-*-v1 capabilities, org-wide or per claudeCodeTools.teams and claudeCodeTools.users, with the same merge rules as hooks. tools (enabled, tiers, followsDefault), updates.mode (auto, directed, off) and lanes.teams / lanes.users (auto, subscription, org; a user beats their team) keep their stored values when omitted; a sent tools or lanes object, {} included, replaces it. Answers 503 when the stored policy cannot be read. |
| get | /admin/wizard-status | config:read | Onboarding wizard progress. |
| put | /admin/wizard-confirm | wizard:write | Confirm an onboarding wizard step. |
Team-policy GET and PUT responses include revision; send it as optional expectedRevision on PUT to get 409 if another admin saved first.
integrationLevel stores org, teams, users, and agents assignments. Values are
gateway, gateway_hooks, or gateway_hooks_mcp. Agent map keys are service-key ids. An
omitted field keeps its stored value; an empty map clears that scope. Seats resolve user, team,
org, then the gateway_hooks_mcp default; agent keys resolve agent, team, org, then default. The
gateway enforces the resolved level per session, and Connect reports the level it applied on its
daily health check.
/admin/connect-health is content-free: hashes, enum tokens, counts, and timestamps only. Every
request with a verified developer key can refresh durable presence, with at most one write per
developer every five minutes on each gateway replica. A Connect version header creates or updates
the seat and its reported version. Traffic without one only refreshes an existing seat and never
erases a known version. Connect sends the header only from the calls that reflect the installed
build: the refresh tick's verify and heartbeat, its daily health report, doctor, and endpoint
enrollment. Hooks and a running MCP server never send it, so a server started days ago cannot roll
the version back. A seat on a key with no enrollment cert still reports through the tick's health
report. Traffic keeps an existing credential lane unchanged. A seat first
discovered from versioned traffic starts in the heartbeat lane. Each row carries the pseudonymous
userHash, last-seen instant, credential lane
(verify or heartbeat), connect version (null identifies a client that has not reported one),
per-check statuses when reported, and a 0 to 100 compliance score against the tools the fleet policy
requires (Required, stored as locked; every other tool is Available, the person's choice).
When Connect reports claude-code/applied_integration_level, its passing check carries the level
in errorClass, and the seat row exposes the latest recognized value as appliedIntegrationLevel
(gateway, gateway_hooks, or gateway_hooks_mcp). The field is absent for older Connect builds
and unrecognized values. The check does not affect the compliance score or failing check count.
Connect applies policy changes on its next tick and for new sessions.
hookArrivalShadow reads back the ranged-trim verdicts Connect
reports on its bets over the last 30 days: pooled, and one entry per ?shape= family, each with
rangedBets, withVerdicts, unpriced (bets by the reason arrival was null) and cells, one per
verdict pair (today, arrival) with bets, readBacks, readRounds, horizonServed,
todayNetMean and arrivalNetMean. With the posture off every bet is one the shipped check placed, so the cells to compare are today: true, arrival: true (both) and today: true, arrival: false (shipped check only). The trims only the arrival check would add place no bet, so they come as arrivalOnly, apart from the cells: precheck and final, each with candidates, netMean, horizonServed and buckets (candidates by floor(log2(characters removed))). precheck is an upper bound, priced as a trim to nothing; final is priced at the gateway's real trim. The whole answer is absent when the bet store cannot answer.
When no tool policy has ever been saved, GET /admin/team-skills omits tools and returns read-only
toolsDefault instead.
Organization MCP OAuth
URL-only MCP clients authenticate to /mcp/org through the WorkOS SSO connection already attached to
the organization. WorkOS AuthKit is the authorization server, and the gateway is the protected
resource.
| Method | Path | Auth | What it does |
|---|---|---|---|
| get | /.well-known/oauth-protected-resource/mcp/admin | public | Admin resource metadata with its own audience and AuthKit issuer. No tool list or identity. |
| post | /mcp/admin | AuthKit bearer | Remote admin MCP. Stateless JSON-RPC; live console role checked per request; 2 MiB body bound. |
| get · delete | /mcp/admin | AuthKit bearer | Authenticate, then return 405 with Allow: POST. No stream or session. |
| get | /.well-known/oauth-protected-resource | public | Return the exact https://<gateway>/mcp/org resource, the managed AuthKit issuer, and bearer-header support. Cache-Control: no-store; bursts return 429 with Retry-After. |
| get | /.well-known/oauth-authorization-server | public | Compatibility proxy for clients that skip protected-resource discovery. Fetches AuthKit's metadata through the outbound SSRF guard and returns it only when the issuer matches and S256 is supported. |
| post · delete | /mcp/org | client key or AuthKit bearer | Serves the organization's remote connectors under the verified caller's identity. DELETE acknowledges session closure with 204. |
| get | /mcp/org | client key or AuthKit bearer | Authenticate, then return 405 with Allow: POST, DELETE. |
Authless tools are shared. When an OAuth connector needs this caller to sign in, a gateway-owned
anyray_signin__<connector> tool appears for it, one per waiting connector with no ceiling. Once a
grant exists, the connector's <connector>__<tool> tools replace it. The retired anyray_mcp__ and
anyray_oauth__ prefixes still route for one release.
anyray_connect accepts three call shapes:
| Call | Result |
|---|---|
| no arguments | Lists the connectors waiting for the caller. Starts nothing. |
{ "connector": "Atlassian" } | Returns an authorization URL. The vendor callback shows a single-use, five-minute claim code. |
{ "connector": "Atlassian", "claim": "ABCDEFGHJKLM" } | Binds the grant to the verified caller who redeems it. |
A usable grant blocks both start and redemption, so the tool cannot replace a working account connection.
GET /admin/settings returns healthDetailShared: true by default. Change only that field with
PUT /admin/settings and {"healthDetailShared":false}. Writes require content:manage and deployment ownership, and changes
are audited. Reads require config:read and deployment ownership. An unreadable store reads false.
The /admin/v1/settings twins use settings:read and settings:content respectively; the write
also works through /admin/v1/settings/content. The floor tier suppresses sharing without changing
the stored choice. See Data boundary.
Two settings govern which sign-in tools appear, both through PUT /admin/settings or
PUT /admin/v1/settings:
| Setting | Default | Effect |
|---|---|---|
mcpConnectorSignInTools | true | Serves one anyray_signin__<connector> per waiting connector, no ceiling. Set false to serve no sign-in tool at all. |
mcpConnectTool | false | Replaces the named tools with one anyray_connect covering every waiting connector, including rows the gateway has not yet classified. |
Each named tool adds roughly 80 prompt tokens to every request against a flat 49 for the shared one,
so an organization with many connectors waiting at once can turn on mcpConnectTool to trade
findability for a shorter prefix. A remembered OAuth tool called without a usable grant returns 200
with result.isError: true and names the connector; it points to a sign-in tool only when one is
actually being advertised, and it never starts authorization.
On a deployment whose SSO sign-in is live, a missing credential returns 401 with:
WWW-Authenticate: Bearer resource_metadata="https://<gateway>/.well-known/oauth-protected-resource"
A deployment with no SSO sign-in advertises no authorization server at all. /mcp/org returns
401 naming the client key and omits WWW-Authenticate, and both discovery documents return 404,
so a URL-only client stops rather than completing a login whose token this gateway can never accept.
Token requirements and the full verification chain: API reference.
A presented x-anyray-api-key, or Authorization: Bearer ark_..., takes precedence over the AuthKit
lane.
/mcp/org accepts one request per POST and returns 405 for GET, so it has no server-push channel.
Its initialize response does not advertise tools.listChanged, so clients must issue tools/list
again after consent, and clients that cache the list may need to reconnect.
anyray-connect contract: /connect/*
Called by the anyray-connect CLI and its hooks. The managed workflow SDK uses the strategy
policy and report routes. Carries savings beacons, policy,
and CCR retrieve/recall. No admin capability; authenticated as the connected client.
| Method | Path | What it does |
|---|---|---|
| post | /connect/verify | Verify a client key or enrollment. |
| get | /connect/policy | Fetch the client's effective policy: shared skills, routing origin, bound team, and the fleet tool set. See below. |
| get | /connect/strategies | Read { revision, strategies: { symbol_context, read_only_workflows } } for the verified key's user, team and tenant. Both switches default off. 404 means the optimizer predates support; 503 means policy could not be read. Read once per new client/app session. |
| post | /connect/strategies/report | Report { clientStrategies: { symbol_context?: { state, revision? }, read_only_workflows?: { state, revision? } } }. Only known runtime states and hash revisions are accepted; the server supplies observation times. Stores per-key metadata, never tool content, names or savings. Does not establish MCP retrieval capability. The key's admin readback exposes the last clientStrategies reports. |
| post | /connect/artifacts | Publish a page to share inside the organization: { title, body, contentType, share? }, where contentType is text/html, text/markdown, image/svg+xml or upload-only text/jsx and share is { org, users, teams } (default: only you). The owner is always the key's own user, so an agent (service) key gets 403 artifacts_person_key_required. 201 returns metadata only, plus url, the page's console link (<console>/console/artifacts/<id>), when the gateway knows its console URL (ANYRAY_CONSOLE_PUBLIC_URL, else its own host on port 3000). 409 artifacts_unavailable names why publishing is closed: turned_off, content_off, no_content_key or no_store. 400 for a malformed page or a share entry that is not a person or team id, and 400 artifact_refused (org_share_off) for share.org while artifactsOrgShare is off; 400 artifact_refused also names unsupported_import, no_default_export, compile_error or too_large for React. JSX needs a default export and imports only from react, react/jsx-runtime, react-dom or react-dom/client; it is compiled to stored text/html (200 KiB source, 2 MiB output, five-second build limit). SVG needs an <svg element. 413 over 5 MiB; 503 artifact_busy when eight builds are already waiting for the two compiler slots, or a wait reaches five seconds. Publishing is limited to 10 requests per minute per key across all content types (429 with retry-after), checked before reading the body. Storage quotas also return 429 once the owner keeps 500 pages or 250 MB. 503 key_store_unavailable when the gateway cannot read its key store, so it cannot verify the key: retry. Needs artifactsEnabled. A team share opens for the people the IdP puts on that team (SCIM, or Directory Sync and SSO groups). |
| get | /connect/artifacts | List the pages shared with the key's own person, newest first: { data, next_cursor, skipped, console_url }, metadata only (no body). skipped counts pages left out because their title would not decrypt; 0 is a complete list. The answer has an ETag over the ids in the window you asked for (and whether older pages follow it), so a page added far behind it leaves the tag alone: send it back as If-None-Match and an unchanged list is 304 with no body. A key may list 20 times a minute (429 with retry-after), and open 120. console_url is the console's artifacts base (<console>/console/artifacts/), left out when the gateway does not know its console URL; append an id to get a page's link. Both reads carry it. Takes limit (default 100, max 500) and cursor; 400 for a cursor that did not come from next_cursor. Uses the console's visibility rule: the owner, named people, a team the IdP puts them on, or the whole organization while artifactsOrgShare is on. 403 artifacts_person_key_required for an agent (service or machine) key, 403 artifacts_person_deactivated for a deactivated person, 409 artifacts_unavailable when pages cannot be read. Works with publishing turned off. Cache-Control: no-store. |
| get | /connect/artifacts/{id} | Open one page: { data } with the body. 404 artifact_not_found for a page that is missing or not shared with the key's person, so the two cannot be told apart. Same refusals as the list. Each open by someone other than the owner is audited with ids only. |
| delete | /connect/artifacts/{id} | Remove a page you published. Works with publishing turned off. 204, or 404 for a page that is not yours. |
| get | /connect/client-assets/{kind}/{sha256} | Download a client build this gateway serves, by its sha256: vscode-extension (the VSIX) or openclaw-plugin (a JSON document of the plugin's files). Key-gated. 404 for a kind or sha this image does not carry. |
| get | /connect/self-check | What the calling key can ask about its own wiring, for an agent host that cannot run Connect. Content-free and scoped to the presented key: gateway.reachable, identity.keyKind (service or personal), retrieval (per-runtime verdicts over the last day, or unavailable when the read failed, or unmeasured_personal_key), key (this key's own traffic evidence), runbookVersion, skillVersion, skill (whether each runtime listed the anyray skill to its model over the last day), and problems as a count. Answers from a 15 second cache, so poll no faster. See below. |
| post | /connect/test-key | Mint a seven-day test key with your personal Connect key. Returns { key, keyId, expiresAt } once. Replaces your previous test key in the same tenant; cannot renew. |
| get | /connect/test/context | Test key only. Returns its tenant, gateway version, evaluation-cohort marker, session-gate counts, and running replicas. |
| post | /connect/test/results | Test key only. Store a content-free benchmark result using the strict /admin/v1/client-tests body. |
| get | /connect/test/results | Test key only. Read its tenant's results; latest=plan selects the newest per plan. limit is 1 to 200 (default 50). |
| get | /connect/test/sessions/:sessionId | Test key only. Read this key's request count and list-price cost for the session, with keep-warm pings counted separately. |
| get | /connect/savings | Report realized savings for the client. |
| get | /connect/usage | The key holder's own dashboard: the four headline tiles in kpis, a daily trend, and tokens kept and saved per model and per tool. At most 6 models and 8 tools are named, fewer when ids are very long; the rest are reported together as (other). The user is always the one the client key belongs to. ?window= (default 30d). A user with no usage gets zeros. 429 when a key polls more than 20 times a minute. 503 when the spend store cannot be read. |
| post | /connect/hook-savings | Record hook-trim savings (PostToolUse) under the verified client key's user, credited to the strategies the beacon names (known strategy ids only), else hook_local_trim. A beacon with no client key, or a rejected one, records nothing, and so does a claim that rounds to 0 tokens. Each user has two hourly caps in any 60 minutes, shared by the three hook lanes and counted per gateway replica: 5M claimed tokens, and 6.25M billed units (5M x 1.25). The units cap admits fewer claimed tokens the more later turns a trim is credited. A beacon that would pass either cap records nothing. A paste beacon (source: "prompt") counts only from Cursor (tool: "cursor"), as advisory potentialPaste that is never billed and not held to those caps. Any other paste beacon records nothing: the optimizer's paste_gap measures those pastes. A beacon that names its tee (tee: { id, shape, lines }) also records the trim as a bet for the ranged lane. A ranged bet may add tee.verdicts, the two verdicts Connect priced it with (today and arrival, true or false, arrival null when it could not be priced), each rule's predicted net (todayNet, arrivalNet, token-equivalents), horizon (served or estimator) and unpriced (why arrival is null); the bet keeps them, and a field outside that shape is dropped. event: "arrival-candidate" with candidate (shape, stage, net, sizeBucket, horizon) reports a ranged trim only the arrival check would admit: a verified key counts it per UTC day under the family's pseudonym, with no saving, spend row, bet or seat. event: "tee-read" with tee.id (plus range.lines for a sized read) marks that bet read back. Both take a verified key and store pseudonyms and counts only. event: "digest-stood-aside" with digest (router, version, kept, total) and originalChars reports a digest the digestEconomics gate held back: a verified key records it on that call's trace as a command_digest decision with standAside: true and no saving, never a spend row (a log_read decision when digest.strategy is log_read, the test-log Read lane's cost gate). An older gateway answers it recorded: false. A saving whose digest.strategy is log_read (with the test-run router) is booked and shown as the log_read strategy; an older gateway ignores the field and records it as a test-run command digest. event: "log-read-declined" with reason (ranged, reread, raw_latched, no_failures, changed, not_whole_file, not_test_output, failures_not_kept, failure_block_cut, page_unbounded, no_saving, would_misnumber_lines or digest_stood_aside), originalChars and tool reports a Read of a test log that lane left whole: a verified key records it on that call's trace as a log_read decision with standAside: true and the declineReason, counts it on the trace's strategyDeclines, and books no saving and no spend row. A reason outside the list records nothing, and at most 600 are recorded per user per hour. A recorded bet gets readBack back when the bet store can answer: that shape across every seat, in the same form as on the ranged reply below, which Connect keeps per shape. A saving may name the request of the session it fired on (requestsSoFar, a non-negative integer; anything else is ignored): the gateway reads the deployment's survival curve at that request to credit the trim's later cache reads. A saving and a stand-aside may name the client whose hook sent them (clientTool, such as claude-code or codex): the spend row, the bet, its readBack and the trace take that id when it is a client tool id the gateway knows, else unknown. Without it they take the request's own client tool (the tool in x-anyray-metadata, else the User-Agent), held to the same ids. A tee-read may carry it too, but its bet keeps the id that placed it. |
| post | /connect/optimize-output | Optimize a tool output at the source: a reversible trim, or a ranged trim for a tenant that turned rangedHookTrim on. Anything else returns a no-op. The saving shares the hook-savings caps (5M claimed tokens and 6.25M billed units per user in any 60 minutes, per replica); past either, the trim still comes back but is not recorded. The body may carry requestsSoFar, the session's request count, read the same way as on /connect/hook-savings. See below. |
| post | /mcp | The gateway as a remote MCP server (streamable HTTP): serves anyray_retrieve and anyray_recall, client-key gated. To Claude Desktop (by its User-Agent) on a person's key, tools/list also lists anyray_list_shared and anyray_open_shared while artifacts.mcpRead on /connect/policy is true. A call to either takes any person's key and follows GET /connect/artifacts: the same visibility rule, tenancy, paging, and audit of an open by someone other than the owner. Every valid authenticated JSON-RPC message renews the key's 6-hour retrieval lease. Each served tools/call counts toward the per-tool adoption bins, the same ones the stdio server's heartbeat fills. GET /mcp and DELETE /mcp answer 405: there is no MCP session to stream or end. |
| post | /connect/recall · /connect/retrieve | CCR recall and retrieve of externalized context. A human or CLI caller sends x-anyray-retrieve-probe: 1 so the call does not latch retrieve capability. |
| post | /connect/mcp-heartbeat | MCP-server liveness heartbeat, which authorizes the deferral_latched evidence class when the tool is hidden behind deferred loading. Optional body { "client": "<name>", "retrieveApproved": <bool> } authorizes approval_latched on clients that never name the tool in the request at all; sending no body leaves a stored approval as it was. |
| post | /connect/health | Record anyray-connect doctor results or the authenticated daily selfcheck, plus the client's own version. The ambient report updates the verified seat and its per-user check bins. Best-effort, 200; the deployment-wide rolling 24h rollup travels on the next meter round. A report without a verified client key (before enrollment, or a rejected key) is recorded only within the per-IP unauthenticated rate (ANYRAY_RATE_LIMIT_UNAUTH_RPM, default 10 a minute); past it a keyless report gets 429. |
| post | /connect/key/heartbeat | Extend the existing enrollment key without replacing it, including recovery after bearer expiry. Needs the current ark_ key, the signed DevCert, and a fresh purpose: "key-heartbeat" proof. The bearer alone is insufficient, and no replacement key is ever returned. |
| post | /connect/machine | Report the machine-to-seat join, which fleet Connect updates read. Gated like /connect/key/heartbeat, with machineUuid as a sibling of the proof. Only sha256 hashes are stored, never the raw UUID, hostname, or username. Off-Postgres nothing is stored (recorded: false). |
| get | /connect/directive | Pull the seat's next remediation directive ({directive:{id,kind,tool?}} or {}). Key-gated and scoped to the caller's own seat; a directive re-delivers until acked. See below. |
| post | /connect/directive/ack | Acknowledge an applied directive ({id, result:"ok"|"failed"}). Scoped to the caller's own seat, so a leaked id cannot close another developer's directive. A revert reporting ok also marks the seat offboarded; the kind is read from the stored directive, never the payload. |
| post | /connect/offboard | Announce that this seat has been reverted off Anyray, sent while the credential it is about to delete still authenticates. Always 200 with {status:"ok", recorded:<bool>}. |
| post | /connect/mcp-oauth/authorize | Start an OAuth browser flow. See below. |
| post | /connect/mcp-oauth/complete | Redeem the code shown in the consenting browser: { "name": "connector name", "claimCode": "12-character code" } → { "status": "ok", "user": "verified account" }. The grant binds to the account redeeming the single-use, five-minute code. |
GET /connect/policy fields
Returns shared skills (the console's Skills and Guidance plus any legacy team skills), routing
origin, bound team, and the fleet tool set (tools.enabled plus a tools.tiers entry for each).
The built-in anyray skill carries variants["claude-code"], Claude Code's version, beside the
shared body. A service key, or ?tool=sdk-agent (also langchain or langgraph), gets the
agent version as body and no variants. Any other tool is ignored. See
which version each machine gets.
A deployment that never saved a tool policy is served the default set, every tool recommended,
with tools.defaulted: true, so a tool installed after enrollment still gets configured. A saved
list with tools.followsDefault: true (what the console's Users → Add User → Required tools writes) is served with every
default tool it lacks added as recommended, so a tool Connect adds later reaches the fleet. A seat whose developer named their own
tools with --tools ignores a defaulted list, and an admin's saved list always wins.
Every enabled tool is served with a tier: locked is Required, anything else is Available. A stored
required or missing tier is served as recommended, and a stored optional is served unchanged.
A seat enrolled from the desktop app takes only locked tools from either list; the person turns on the rest.
skillsIncomplete: true means a skills store could not be read this call, so clients write what is
listed and remove nothing.
With ?upstream=<origin> the response adds upstream.authorized: whether that host is allowed
as a custom host here. Connect asks before it keeps a developer's own
LiteLLM key. The allowlist is never returned.
personalUpstream: { "url": "https://litellm.example.com" } is the saved
personal upstream. It is served to a personal developer
key only, never to a service or agent key, and left out when the setting is off.
seat.anthropic.rejected lists the caller's own Claude subscription tokens Anthropic refused,
newest first, at most 5: fingerprint, rejectedAt and reason. Connect sets aside the stored
token whose fingerprint matches. Omitted when there are none, and by an older gateway. See
rejected subscription tokens.
artifacts: { "publish": true } means this key's POST /connect/artifacts would be accepted now:
a person's key while artifactsEnabled is on and content
can be stored. An agent (service) key always reads false. A gateway that predates sharing omits
the field; read that as off. It does not cover share.org, which
artifactsOrgShare can refuse.
artifacts.read is true when GET /connect/artifacts would answer for this key: a person's
key, with the content store and key present. It stays true while publishing is off. A gateway
that predates reading omits it; read that as off.
artifacts.mcpRead is true when POST /mcp lists anyray_list_shared and
anyray_open_shared to Claude Desktop on this key. It equals read. A gateway that predates it
omits it, and its /mcp serves neither tool.
hooks.digest and hooks.logRead default to on. hooks.readTrim, hooks.lifecycle,
hooks.prompt, hooks.digestEconomics, hooks.rangedArrival and
hooks.fleetReadBack default to off.
Each takes on or off.
Digest shortens command output; Read trim requires anyray_retrieve; lifecycle records
session receipts; logRead shortens a Read of a
failing test log the same session's test command wrote; digestEconomics holds back a Claude
Code command digest, or a logRead digest, whose expected read-backs cost more than it saves,
and needs digest or logRead on; rangedArrival lets the ranged-trim check priced on the
hookArrival rates below decide which ranged trims go out; fleetReadBack passes a command's
output through undigested when the fleet keeps reading that kind of digest back, priced per output
size. prompt is retired: Connect
ignores it and removes its old paste hook. Optional hooks.teams and hooks.users assign switches per identity. Each switch
resolves user, then team, then org, then default. Seats receive only their eight resolved values,
never the maps. An unreadable store serves all eight as off. When an older gateway omits
hooks, Connect keeps its cached posture.
hooks.sessionGate carries the session gate's
connect_hook posture: mode (on or off) and, while the lane is measured, draw
(seed, 32 hex characters, and fraction, at most 0.5). Connect draws one arm per Claude Code
session from them, keyed on the hook's session_id. Codex and Cursor hooks cannot name that
session, so they are in neither arm and follow mode. A Connect that sends
x-anyray-connect-capabilities: session-gate-v1 honors it; an older one ignores it. Absent while
the gate's state is loading, and Connect then keeps the gate it had.
mcp.org.url is the organization MCP endpoint when the gateway has a valid
public HTTPS origin. Optional ?model=<id> also returns modelContext.contextTokens, that model's
real context window, omitted when the model is unknown. It also returns hookArrival, the
model it was asked for and its rates: read, write5m, write1h and output, the
prompt-cache read, five-minute and one-hour write, and output rates, each relative to one fresh
input token and derived from the model's price entry. A model the gateway prices only by fallback
gets no hookArrival. Connect prices the ranged-trim check with them. With ?clientTool=<id> (a
client tool id such as claude-code), hookArrival.horizon.main is the measured turns left on
the main thread: for each count of requests so far, the most further requests three in four of
your tenant's sessions of that client made before their first compaction. It is served only
where mintSessionSurvival is true or "shadow"
and the curve is built; otherwise it is absent and Connect estimates the horizon itself. At most
1,000 points; past the last, the last holds.
integrationLevel reports {level, source} for this caller. source is user, agent, team,
org, or default. An unreadable policy store reports gateway_hooks_mcp from default.
Existing clients ignore the field.
capabilities lists gateway features Connect may turn on in a tool's own config.
responses-websocket-v1 means the gateway relays Codex's Responses WebSocket. request-zstd-v1
means it accepts zstd-compressed request bodies. An older gateway omits the field, and Connect
leaves those features off.
Two more are opt-in and off by default. claude-code-deny-first-party-tools-v1 has Connect add
WebSearch and DesignSync to Claude Code's permissions.deny.
claude-code-unpinned-tool-search-v1 has Connect leave env.ENABLE_TOOL_SEARCH unset. Each is
served only when its claudeCodeTools switch is on for the caller and the default routing config
reaches only Bedrock, with no custom host. Subscription-lane seats and agent service keys never
get them.
clientAssets names the VS Code extension and OpenClaw plugin builds this gateway image serves:
version, sha256, and bytes for each, plus distSha256 (extension) or sourceSha256
(plugin), and on a release image Anyray's Ed25519 signature over those fields. Connect installs
a served build only when the signature verifies against a key built into Connect and the build is
newer than its bundled copy and any served build it holds. It fetches the bytes from
/connect/client-assets/{kind}/{sha256}, checks the sha256 first, and only over HTTPS (or plain
HTTP on loopback). On any failure it keeps the last verified build, else the bundled copy. An
older gateway omits the field, and an unsigned entry is never installed.
States GET /connect/self-check can report
retrieval.state is measured, unavailable, or unmeasured_personal_key. measured carries
windowHours and a runtimes array, one entry per client runtime the key has used, each with a
clientTool, a verdict, a request count, and counts by evidence class. An empty runtimes is its
own answer: this key has sent no request the gateway could classify yet. unavailable means the
evidence read failed, timed out, or was capped, so nothing is proven either way.
unmeasured_personal_key means the key is a developer key, which stamps no agent binding on its
rows, so there is no per-runtime evidence to read. A measured report also carries readBack:
handles minted and reads that resolved or missed per tool
(Retrieval read-back). It does not change problems.
A verdict is wired, unlatched, unwired, toolless_only, parity, or unknown, with the
meanings the Agents card shows.
A parity runtime also carries
readPath, which
never changes problems. problems counts the runtimes that are not wired, toolless_only, or parity (forwarded
unchanged by the deployment's default), and scores 1 for each of the three shapes that prove
nothing: an empty runtimes, unavailable, and unmeasured_personal_key. So problems: 0
means the store answered, at least one runtime was seen, and every runtime seen is wired or has
nothing to wire.
key.state is reported or unavailable (the key record could not be read). reported carries
the metadata the console's Health pill reads for this key, never a key id, owner or hash:
expiresAt, renewMs, firstSeenAt, lastSeenAt, lastSeenTool, retrieveCapableAt,
sourceHook, the four sourceTrim* fields, clientTools, and connectRefresh. Stamps a key has
not earned yet are left out. An older gateway omits key.
Pass ?tools=claude-code,codex (comma-separated, up to 16 tool ids) to add toolTraffic for the
calling key's user over the last 7 days. toolTraffic.state is reported or unavailable (the
read failed). reported carries windowDays: 7 and tools, keyed by tool id, each with
hookRows, inferenceRows and optimizeRows. optimizeRows counts the optimize-only calls a Copilot seat
makes, which the other two never include. An older gateway omits optimizeRows.
deviceRequests says whether the gateway can answer for the calling key's own device. reported
means the key is bound to an enrolled device: each tool then carries deviceLastRequestAt, the
ISO 8601 UTC time of the newest inference request of any outcome that this device sent, and a tool
without it sent none in the window. Hook and optimize rows never set it, and neither does the same
person's other machine. unsupported means the key is bound to no device, so no time is given.
An older gateway omits deviceRequests.
An older gateway omits toolTraffic, which Connect shows as Ready with "Your gateway can't report this yet" under Traffic.
runbookVersion and skillVersion are what this gateway release ships with. An agent records both
at setup and re-reads the agent-host runbook or re-pulls
GET /connect/policy when either goes up.
Directive kinds a client can run
The client declares what it can run in x-anyray-directive-kinds, a comma list. Absent means the
three original kinds. A kind it did not advertise is skipped, not delivered: an unrecognized
directive is never acked and would park at the head of the seat's queue, swallowing every later
one. It stays queued and lands once the client updates. Supported kinds are reapply_config,
remint, revert, revert_tool, and update.
Loopback fields on /connect/mcp-oauth/authorize
Body: { "name": "connector name", "loopbackPort": 49152, "loopbackNonce": "<43 base64url chars>" }.
The loopback fields are optional but go together: a port from 1024 to 65535 that Connect is
listening on at 127.0.0.1, and 32 random bytes Connect minted for this run. One without the other
answers 400.
Returns { "authorizationUrl": "https://..." }, plus loopbackPort echoed back when accepted, so
Connect can tell a gateway that predates the field. Starting the flow binds no user. After consent
the callback shows the claim code as text/plain when no port was given, and otherwise answers
302 to http://127.0.0.1:<port>/callback?claim=<code>&nonce=<SHA-256 of loopbackNonce, base64url>
with cache-control: no-store. Connect redeems the code only under its own nonce digest, so a code
from another run pushed at that port is ignored.
Trimming lanes on /connect/optimize-output
Two lanes trim. Everything else returns the confirmed byte-identical no-op
{"status": "ok", "optimized": false, "output": …, "handled": true} and records no savings.
| Request | Result |
|---|---|
reversible: true with retrievalContext: "available" | A trim with retrieve ctx_… handles. The saving is recorded server-side. An anyray_search result is trimmed only when the key has called anyray_retrieve within the last 6 hours. The answer lists strategies (kind, tokensSaved), which Connect sends back when it reports a replay of this trim. |
ranged: true (not reversible, not a managed source hook) | A trim whose omitted runs read … [N line(s) omitted by anyray · lines A-B], only for a tenant with relevance_filter rangedHookTrim on. The response adds rangedHandled: true, plus rangedDisabled: true when the tenant has not turned it on, and readBack when the bet store can answer: for a request that names its command shape, that shape's ranged bets across every seat (emits, rereads, readRounds, sliceSum, slices) and its digest bets (emits, rereads), plus the pooled read-back rate. Every figure is narrowed to the caller's client tool. Connect's trim rules still decide from its own ledger. No saving is recorded: Connect reports it through /connect/hook-savings only if it emits the trim. |
Connect sends retrievalContext from the current hook consumer, not from a long-lived client
identity. A reversible: true request whose value is unavailable, unknown, invalid, or absent
gets the no-op. The probe answer (no output) adds rangedLane: true on a gateway that serves the
ranged lane. Connect sends ranged: true only to such a gateway, and skips the lane for an hour
after rangedDisabled. A trim with neither a handle nor line ranges is gone: a lossy trim of
arbitrary output was often read back in full right away, which cost more than the original.
The anyray_connect tool inside /mcp/org uses these same authorization and claim operations. OAuth
state stays userless: the vendor callback stages encrypted tokens behind the short claim code, and
only redemption by a verified client-key or AuthKit identity creates the grant.
/connect/health also accepts x-anyray-connect-login-observation, a JSON header bounded to 256
characters: { "version": 1, "state": "enabled", "observedAt": "2026-09-08T10:00:00Z" }. States are
enabled, disabled, approval-required, unknown, and error. It needs an existing valid client
key, and identity comes from that key. The response adds loginObservationRecorded, and
/admin/connect-health exposes the latest per-user observation as
loginRegistration: { state, observedAt }, independently of seat presence and compliance. Older
observations and heartbeats cannot replace a newer observation. See
desktop reporting cadence for freshness handling.
/connect/health accepts x-anyray-org-plugin-states, a JSON snapshot shaped as
[{tool, plugins: [{id, version, state}]}] for Claude Code and Claude Desktop. Each report holds
at most 32 plugins total (8 KB maximum); ids and versions are at most 64 characters each, and an unknown version
is null. Records contain tool, id, version and an
allowlisted state, never paths or file contents.
Only a verified human client key updates the seat's shared Postgres snapshot. The admin install-state
read keeps each machine's latest report from the last 72 hours, so a laptop that reports daily is not dropped for a weekend, and combines a seat's machines into one.
The daily selfcheck sends its existing client key and
x-anyray-connect-health-kind: ambient. A verified ambient report records the same events in the
seat's per-user bins. A doctor aggregate omits that discriminator because its separate verified
doctor lane already records those bins. Missing or rejected keys never create or update a seat.
Authenticated /connect/verify health reports may also send
x-anyray-connect-residue, a JSON array of the closed installer-residue enum. The header is
omitted when Connect did not probe, [] is an explicit clean result, and any member except
osquery_log_retained is actionable residue (Connect reports that one for information only). The
latest explicit value is preserved across heartbeats that omit the header. The admin seat endpoints
expose it as residue and derive state: "needs_pkg_install" only when an actionable member is
present.
The same check-ins may send x-anyray-connect-time-zone, the machine's IANA time zone (such as
Asia/Jerusalem). The gateway keeps it only when it is a zone the runtime knows, stores it on the
seat, and keeps the last value when a check-in omits it. /admin/spend/activity returns it per
person as timeZone; a person with no report reads not_reported.
/public/* (admin-token sign-in: /public/auth, /public/auth/session, /public/logs) and
/log/stream back the gateway's built-in log page, not the console, and are not a public API.
A wrong admin token at /public/auth counts toward the
failed-auth limit. For automation use /admin/*
with a scoped admin API key, which is capability-limited,
or the admin token.
GET /v1/me also exposes key-scoped source-trim timestamps and a request count, without content or
key material. Source evidence fields.