Skip to main content

Endpoint reference

The complete map of the gateway's HTTP surface: /v1/, /scim/v2/, /admin/*, /connect, /device.

Auth lanes
SurfaceGated by
/v1/*An enrolled client key (minted by anyray-connect) or a non-human service key; attribution rides the x-anyray-metadata header.
/mcp/orgThe caller's client key, or a WorkOS AuthKit access token whose audience is this gateway's exact /mcp/org URL.
/admin/*The admin token (ANYRAY_ADMIN_TOKEN, the break-glass owner), a role-scoped console SSO session, or a scoped admin API key (aak_…; see the API reference). Authenticated ≠ privileged: each route additionally requires the capability in its row, per the RBAC lattice (viewer → auditor → operator → security_admin → owner).
/scim/v2/*Its own static bearer, configured through PUT /admin/scim/settings; not an admin-console session.

In fleet mode (ANYRAY_MULTI_TENANT=true) most configuration routes additionally require the deployment owner (requireDeploymentOwner(); a tenant SSO principal gets 403 regardless of role). Tenant-scoped reads such as /admin/spend*, /admin/health, /admin/me, /admin/client-keys, /admin/service-keys, and the optimizer/observability routes stay role-gated only. Rows below mark deployment-owner routes where the distinction most often bites.

Inference requests and the VS Code pre-call hook carry content on the request path under the deployment's content mode; SCIM identities, admin spend records, and request logs never expose prompt/response bodies.

Read the capabilities from the OpenAPI spec

Every /admin/* operation in the OpenAPI spec carries an x-required-capability field holding 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 listed 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 below apply unchanged; only the path, the gate, and the error envelope differ. The generated OpenAPI spec carries x-required-scope, x-scope (tenant or deployment), and x-legacy-operation on every v1 operation.

ResourcePathsScopesLegacy twins
meGET /admin/v1/menone (any admin principal)/admin/me
keys/admin/v1/keys (?type=service), /admin/v1/keys/{id} (GET/PATCH/DELETE), …/{id}/rotate, …/{id}/secretkeys:read, keys:write, keys:secret/admin/client-keys, /admin/service-keys
admin-keys/admin/v1/admin-keys, /admin/v1/admin-keys/{id}admin-keys:read, admin-keys:write/admin/api-keys
users/admin/v1/users/limits, /admin/v1/users/{user}/limits, /admin/v1/users/revoked, /admin/v1/users/revoked/{email}, /admin/v1/users/{user}/export, /admin/v1/users/{user}/datausers:read, users:write, users:revoke, users:gdpr/admin/user-caps, /admin/revoked-users, /admin/gdpr/users/…
spend/admin/v1/spend/summary, …/dashboard, …/usage, …/usage/properties, …/quality-parity, …/externalspend:read/admin/spend, /admin/spend/*, /admin/spend-connectors/usage
providers/admin/v1/providers (GET is providers:write: keys come back in full; /admin/v1/providers/capabilities is the credential-free view on providers:read), …/{slug}/key (PUT/DELETE), /admin/v1/providers/connectors, …/connectors/syncproviders:read, providers:write/admin/provider-keys, /admin/spend-connectors
routing/admin/v1/routing, /admin/v1/routing/aliases, /admin/v1/routing/modelsrouting:read, routing:write/admin/routing-config, /admin/model-aliases, /admin/pricing
optimizer/admin/v1/optimizer, /admin/v1/optimizer/purge, GET /admin/v1/optimizer/session-gate, PUT /admin/v1/optimizer/session-gate/{kind}optimizer:read, optimizer:write/admin/optimizer/settings, /admin/optimizer/purge, GET /admin/optimizer/session-gate, PUT /admin/optimizer/session-gate/{kind}
policies/admin/v1/policies/teams, /admin/v1/policies/claude-desktop, …/claude-desktop/mobileconfig, …/claude-desktop/connectors/{probe,readiness,authorize}policies:read, policies:write/admin/team-skills, /admin/claude-desktop-policy, /admin/claude-desktop-policy/connectors/*
settings/admin/v1/settings, …/content, …/updates, …/setup, …/health, …/support-bundle, …/support-bundle/sharesettings:read, settings:write, settings:content, settings:update/admin/settings, /admin/update-status, /admin/update/settings, /admin/wizard-*, /admin/health, /admin/support/bundle
sso/admin/v1/sso, /admin/v1/sso/scim, /admin/v1/sso/portal-linksso:read, sso:write/admin/idp-config, /admin/scim/settings, /admin/workos-portal-link
enrollment/admin/v1/enrollment/links, …/links/{id} (?hard=true)enrollment:read, enrollment:write/admin/enrollment-links
provisioning/admin/v1/provisioning/tokens, …/tokens/{id}, …/tokens/{id}/rotateprovisioning:read, provisioning:write/admin/provisioning-tokens
fleet/admin/v1/fleet/status, …/org, …/hosts, …/hosts/{id}, …/hosts/{id}/remediate, …/policies, …/scripts, …/policy-push, …/enroll-secret, …/evidence-connectors, …/mdm-servers, …/mdm-profile, …/installers, …/seats, …/seats/query, …/directivesfleet:read, fleet:write, fleet:enroll, fleet:remediate, fleet:direct, fleet:content/admin/endpoint/*, the lane-agnostic /admin/endpoint-fleet/* rows, /admin/connect-health, /admin/connect/directives
traces/admin/v1/traces, …/facets, …/sessions, …/{id}, …/{id}/contenttraces:read, traces:content/admin/observability/*
audit/admin/v1/audit/events ({data,next_cursor} pages), /admin/v1/audit/auth-events, /admin/v1/audit/meteringaudit:read/admin/auth-events, /admin/metering/last-report
playgroundPOST /admin/v1/playgroundplayground:run/admin/test-request

Not re-served: the console session routes (/admin/auth, /admin/auth/update, /admin/sso/*), the legacy /admin/spend/connectors alias, and the self-hosted Fleet-server lane (/admin/endpoint-fleet config, /sync, /pack, its hosts/{id}, run-script, and enroll-secret), which is retired with that lane. GET /admin/v1/traces/{id} is metadata-only for every caller; …/{id}/content is the one trace path that returns bodies, and only to a principal holding traces:content (never a key).

Inference API: /v1/*

OpenAI-compatible, plus the Anthropic-native Messages API. Point any OpenAI/Anthropic SDK's base URL at the gateway. Fully described in the OpenAPI spec and the API reference.

On the Anthropic lanes (/v1/messages, and /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, so 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 actually supports. Values at or under the ceiling, unknown model ids, and subscription passthrough traffic pass through unchanged; the value is never raised.

MethodPathWhat it does
get/v1/me · /v1/me/usage · /v1/me/limits · /v1/me/modelsSelf-service for the verified key holder: identity + key metadata, month-to-date tokens/savings, the caps/budgets that would 402 this identity, and the models your admin config makes selectable (every registered alias, plus models a conditional routing lane names outright — the id and its token limits; the provider that serves it stays behind config:read on the admin routes). Answered by the gateway itself (no provider); metadata only.
post/v1/chat/completionsChat completions (streaming + non-streaming).
post/v1/seat/cursor/chat/completionsCursor OpenAI-BYOK lane, 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; store, previous_response_id, and truncation are 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. Configured by anyray-connect; not called by hand.
post/v1/extensions/vscode/optimizeOptimize a VS Code extension request before the extension calls its provider. Fails open and does not record client-supplied usage; the optimizer's own savings are metered as a zero-token spend row.
post/v1/completionsLegacy text completions.
post/v1/embeddingsEmbeddings.
post/v1/messagesAnthropic-native Messages API.
post/v1/messages/count_tokensToken counting (no inference; not metered).
post/v1/responsesResponses API, create (GET/DELETE /v1/responses/{id}, GET /v1/responses/{id}/input_items).
post/v1/audio/speechText-to-speech.
post/v1/audio/transcriptions · post /v1/audio/translationsAudio transcription / translation.
post/v1/images/generations · post /v1/images/editsImage generation / edits.
post/v1/filesUpload a file (GET list; GET/DELETE /v1/files/{id}, GET /v1/files/{id}/content).
post/v1/batchesCreate a batch (GET list; GET /v1/batches/{id}, POST /v1/batches/{id}/cancel).
get/v1/modelsList available models.
post/v1/copilot/:plan/:seat/*GitHub Copilot seat carrier lane (GET too). anyray-connect points the Copilot CLI here; the gateway forwards the seat token so GitHub still bills the Copilot plan.
get/v1/realtimeRealtime (WebSocket) passthrough.

Inbound SCIM v2: /scim/v2/*

Okta, Microsoft Entra ID, and other SCIM clients push verified users and groups to these gateway routes. Every request sends the SCIM bearer as Authorization: Bearer <token>. SCIM responses use application/scim+json; errors use the SCIM 2.0 error envelope.

Discovery

MethodPathWhat it does
get/scim/v2/ServiceProviderConfigAdvertise PATCH, filtering, paging, and bearer authentication.
get/scim/v2/ResourceTypesList the User and Group resource types.
get/scim/v2/SchemasReturn the supported core User and Group schemas.

Users

MethodPathWhat it does
post/scim/v2/UsersCreate a user from the verified IdP payload.
get/scim/v2/UsersList users. Supports filter=userName eq "dev@example.com", startIndex, and count.
get/scim/v2/Users/:idRead one user.
put/scim/v2/Users/:idReplace a user.
patch/scim/v2/Users/:idPatch activation, display name, or identity type. active:false blocks the user's client keys.
delete/scim/v2/Users/:idSoft-delete and deactivate a user so existing client keys stay blocked.

Groups

MethodPathWhat it does
post/scim/v2/GroupsCreate a group and resolve or create its members.
get/scim/v2/GroupsList groups with startIndex and count paging.
get/scim/v2/Groups/:idRead one group and its members.
put/scim/v2/Groups/:idReplace a group and recompute member team/role mappings.
patch/scim/v2/Groups/:idAdd, remove, or replace members; update the display name.
delete/scim/v2/Groups/:idDelete 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. Distinct from the inference /v1/* above: usage rollups, lease state, release metadata, or an explicitly customer-submitted redacted support bundle; never prompt/response content.

MethodPathWhat it does
post/v1/meterIngest a usage rollup (+ deployment heartbeat); returns a freshly signed entitlement lease.
get/v1/entitlementLease heartbeat. Refresh the signed lease without ingesting usage; ?deploymentId= binds it like a meter report does. Throttled per deployment on its own clock, separate from /v1/meter; a 429 carries Retry-After.
get/v1/tenant-contextFleet mode only: resolve an ark_ key hash to its tenant context.
get/v1/releases/latestReturn the current release-line version and its bounded requirements manifest; manifest may be null.
post/v1/support-bundlesAccept a reviewed, redacted support bundle (deployment bearer, 1,000,000-byte cap, one per deployment per minute), analyze it, and return { receiptId }.
post/v1/connect-eventsGateway-down fallback for connect-health: anyray-connect posts its doctor results here when its gateway is unreachable. Authenticated by a DevCert and proof of possession from its bound Ed25519 or device P-256 key (not a deployment bearer); the deployment identity comes from the signed cert. Best-effort, throttled, never on the billing path.
post/v1/cert/renewRoll a live DevCert forward, so a machine in use never has to re-enroll; only a genuinely idle one reaches expiry. Authenticated by a signed challenge proving possession of the enrolled key (the same DevCert proof as above, not a deployment bearer); identity and deployment come from the signed cert. Refused for a revoked developer or a revoked deployment: renewal is never a way around revocation. An SSO-issued certificate is also refused when the tenant's SSO policy is absent or unreadable, rather than falling back to a longer default key lifetime.
post/v1/cli-sso/startStart the Billing app's deployment-authenticated identity half of a CLI login. Receives the gateway session id + user code; returns the browser activation URL.
post/v1/cli-sso/pollReturn pending or the WorkOS-verified identity, governed team list/default, resolved role, and configured key lifetime. Never returns the gateway key.
put/v1/enrollment-identitiesRecord a deployment-verified roster identity with allowlisted source and type. Used by gateway SCIM reflection.

End-point control service

A separate service beside the gateway in your deployment, not part of the gateway or the Billing app. It provisions its single org on boot and re-templates its policy pack whenever the image's pack version changes. The gateway reaches it at ANYRAY_ENDPOINT_CONTROL_URL with the service admin token (ANYRAY_ENDPOINT_CONTROL_ADMIN_TOKEN, falling back to ANYRAY_ADMIN_TOKEN) and re-serves its management plane to the console as the capability-gated /admin/endpoint/* rows in the Health & settings table below, so fleet evidence stays in the deployment's own Postgres and Anyray receives only the metering rollup. Your edge routes exactly the three /api/* families the signed Fleet agent protocol uses, on the same hostname the gateway and console already serve; those paths are anchored at the root by the agent protocol (not configurable), and the gateway serves no /api/* route, so nothing collides. Everything else stays inside the deployment. The Exposure column below is that split.

MethodPathExposureWhat it does
get/livezIn-deploymentProcess-only liveness, used by the container health check. Checks no dependency and stays 200 while draining. Deliberately not routed at your edge: the agent plane shares the gateway's hostname, and the gateway serves its own /livez, which is the one your monitoring should point at.
get/readyzIn-deployment / edge healthReadiness. Pings Postgres and answers 503 during a database outage or graceful drain.
post/api/v1/osquery/*Public, node-key gatedosquery enrollment, configuration, distributed policy work, and content-discarding logs.
post/api/fleet/orbit/*Public, node-key gatedOrbit enrollment, configuration, device-token acceptance, and leased remediation delivery.
Multiple/api/v1/evidence/*Deployment endpoint host, aer_ connector bearerCustomer-managed evidence: fetch the enabled policy catalog (GET …/policies) and report normalized pass/fail snapshots (POST …/report). This lane caps request bodies at 64 KiB (tighter than the 512 KiB general cap below); a larger report is rejected with 413.
Multiple/admin/*Admin bearer; in-deployment on a shared front doorOrg provisioning, policy controls, host views, secret rotation, MDM-profile minting, and allowlisted operator remediation. Your gateway calls these over the in-deployment address. Where the deployment has one front door that routes by path (Docker Compose, Kubernetes, the AWS quick launch), the edge forwards only the agent-plane paths, so an internet request never reaches these. On Railway it is the bearer alone: each service gets its own public domain with no path routing, so the service origin is internet-reachable. See the note below.
On Railway, the admin plane is reachable from the internet

Railway gives every service its own public domain with no path-level routing, so the whole endpoint-control origin is published, not just the agent-plane paths. The admin bearer is the only control on /admin/* there, and it defaults to the same value as ANYRAY_ADMIN_TOKEN, your break-glass owner credential. Treat that token as internet-facing on this platform: set ANYRAY_ENDPOINT_CONTROL_ADMIN_TOKEN to a distinct high-entropy value and rotate it on the same schedule as any other published credential. Every other install path keeps this plane in-deployment by construction.

JSON request bodies are capped at 512 KiB. The public ALB has a high-threshold AWS WAF per-IP volumetric backstop sized for fleets that share a corporate NAT address. Root remediation is bounded at-least-once delivery: a ten-minute lease, three attempts maximum, and a 24-hour execution lifetime. Script output is accepted for Fleet wire compatibility but never stored. Audit rows retain metadata and exit codes only.

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 returned once by start; the secret travels in a header, not the URL, and is stored by the gateway only as a salted hash.

MethodPathWhat it does
post/sso/cli/startCreate 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/:idWith x-anyray-poll-secret, return pending, the eligible team list, or the one-time minted key. Send the selected eligible 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 confirm the terminal code, complete WorkOS SSO, and JIT-provision the identity; they are HTML/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 rather than the completion or poll secrets.

MethodPathWhat it does
post/sso/desktop/startAccept { 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/:idWith 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. A completed response is replayable until the ten-minute grant expires.

Opening /sso/desktop/activate/:id only shows a code form. The browser posts the code to /sso/desktop/authorize; 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 either completion_secret, error=access_denied, or error=no_company. A rejected desktop login never deletes a WorkOS user. Poll key proof signs the versioned canonical message documented in billing/PROTOCOL.md; missing or invalid proof receives the same 404 as an unknown grant. Transient poll 503 responses carry retryable: true; configuration failures do not.

Health & settings

MethodPathCapabilityWhat it does
get/NoneReadiness. Answers 200 normally, 503 ("AI Gateway draining") once graceful shutdown starts, and 503 ("AI Gateway awaiting entitlement lease") while a metered gateway holds no entitlement lease at all (an expired or suspended lease stays 200). Point your readiness check here.
get/livezNoneLiveness. Answers 200 for as long as the process can serve, including while draining, and checks no dependency. Point your liveness check here, never at /. See the note below.
get/docsNoneThe interactive API explorer (Swagger UI) over this gateway's own OpenAPI spec. Exposes only the API shape; "try it out" calls carry whatever credential you enter, so every real route keeps its own auth.
get/openapi.jsonNoneThe machine-readable OpenAPI 3.1 spec of this gateway's /v1/*, /admin/*, and /connect/* surfaces. Load it into Postman or a client generator.
get/admin/healthconfig:readLiveness, spend-store ping, schema version (applied vs expected), optimizer reachability, and the portal leg: lease status plus where this replica's lease came from (leaseSource, leaseTrust, leaseAcquire).
get/admin/meNoneThe caller's identity plus deploymentOwner, which is false for tenant SSO principals in fleet mode (authenticated, no capability required).
get/admin/settingsconfig:readRead Postgres-backed runtime settings, including mcpConnectTool. It defaults to false.
put/admin/settingscontent:manageUpdate runtime settings (contentMode, heartbeatTier, seatOverflow, seatOverflowHourlyMaxRequests, storeCapacityBytes, mcpConnectTool). Set { "mcpConnectTool": true } to advertise anyray_connect when a caller has an OAuth connector waiting for sign-in. This and the console are the only ways to set the gateway's 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 instead of the 10Gi volume the Helm chart ships by default. Give it the size the volume can grow to, such as an RDS storage-autoscaling maximum. Because the alert divides by it, it needs settings:content on /admin/v1, and it is reported as a declaration, not a measurement.
put/admin/settings/identityidentity:manageSet how /v1/* establishes who is calling: {"identityMode": "enrolled"|"network-trusted"}. enrolled (the default) 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; that also turns off SCIM offboarding enforcement and leaves the server-held provider keys reachable by anything on that network. 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 (ANYRAY_MULTI_TENANT=true) resolves identity through the control plane, which never consults this setting; /v1/* there still requires a valid tenant client key and a key-less request still returns 401 missing_key, whatever the mode says. Separate from PUT /admin/settings because it needs its own capability, which no scoped admin API key may ever hold. Audited.
get/admin/metering/last-reportconfig:readExact final payload from this process's last successful, verified usage report, plus the active heartbeat tier and metering state.
get/admin/update-statusconfig:readSelf-updater status: available vs running version, the local preflight verdict (missing environment-variable names + breaking notes), the computed updateClass (soft = image-only, self-applies wherever the deployment has an applier · hard = operator action required, never applied unattended), the autoSoftUpdates policy, hasApplier (whether anything here can apply an update unattended), and the last unattended apply attempt.
put/admin/update/settingsupdate:runUpdate policy: {"autoSoftUpdates": true|false}, whether soft (image-only) updates apply unattended. Applies wherever the deployment has an applier (Compose, and the AWS quicklaunch stack); platforms without one are notify-only. Default on.
get/admin/endpoint-fleetconfig:readRead the end-point lane's Fleet server connection (server URL, policy → remediation-script map, cooldown). Credentials are never returned; presence booleans only.
put/admin/endpoint-fleetprovisioning:manageUpdate the Fleet server connection driving the failed-policy self-healing loop. Validates everything before applying anything. When no webhookSecret is supplied, one is generated and returned exactly once in this response.
delete/admin/endpoint-fleetprovisioning:manageRetire the stored self-hosted Fleet connection. The stored config has unconditional lane precedence, so retiring it is how a deployment cuts over to the in-deployment endpoint-control service: the very next status read follows the normal lane precedence. Idempotent (deleting when nothing is configured is a 200 no-op). Enrolled hosts are not touched; the webhook lane starts answering 503 for stray deliveries. Audited.
get/admin/endpoint-fleet/statusobservability:readRead-only end-point fleet summary. Lane precedence: a stored 0054 Fleet config wins; else the in-deployment endpoint-control service (ANYRAY_ENDPOINT_CONTROL_URL) is read directly, and an unreachable local service degrades in place rather than answering from anywhere else. Successful reads are cached in-process for 15s. An unreachable upstream answers {configured: true, reachable: false} rather than an error.
post/admin/endpoint-fleet/user-machinesobservability:readPer-user end-point machine compliance for the console's Machines page. The caller supplies its roster identities; the gateway resolves identity → pseudonym → machine-seat rows → the end-point host with the same hardware-UUID hash entirely server-side. No hash is mapped back to an identity the caller did not supply, and no hash of any kind is returned: the response is keyed by the supplied emails and carries hostnames, statuses, and counts only. The host join is resolved by exactly the hashes the seats reference (the local lane's targeted by-hash lookup), so it stays complete on any fleet size; a seat whose machine is past the status page's cap still resolves. A machine that joins to no end-point host record carries no compliance claim. When the deployment's lane, not the machine, is why there is no host, the response carries joinUnavailable naming the cause: outdated-image (the endpoint-control image predates the by-hash join; update it), legacy-fleet-row (a stored 0054 self-hosted Fleet config still wins lane precedence and carries no join key; retire it to finish the cut-over), or relay-lane (the Anyray-hosted lane never carries a device identifier; per-user machine compliance needs the in-deployment endpoint-control service). endpointControlOutdated: true is still sent alongside joinUnavailable: 'outdated-image' for older consoles. A miss on a lane that CAN carry the key sets neither; that machine's host was retired. Read-only despite the POST (the roster does not fit a query string).
get/admin/endpoint-fleet/hosts/:idobservability:readContent-free per-host policy detail (hostname, status, evidence source, policy results, and script-run metadata) for the console drill-in. Degrades to {reachable: false} when the selected upstream is unavailable.
post/admin/endpoint-fleet/hosts/:id/run-scriptprovisioning:manageRun a mapped remediation script on one enrolled host from the console. The Fleet compatibility lane remains cooldown-guarded; endpoint-control queues the execution server-side. Customer-managed evidence hosts return 409 monitor_only. Configured actions are audited.
get/admin/endpoint-fleet/packconfig:readBundled vs. applied org policy-pack version (a drift indicator).
post/admin/endpoint-fleet/syncprovisioning:manageApply the bundled org policy pack (policies, remediation scripts, failing-policies webhook) to the configured Fleet server. Idempotent and non-destructive: policies upsert by name and the webhook is wired back at this gateway with the stored secret.
get/admin/endpoint-fleet/enroll-secretenrollment:manageRead the org's Fleet enroll secret, live from the Fleet server (never stored in Anyray). Credential-grade.
post/admin/endpoint-fleet/enroll-secret/rotateenrollment:manageRotate the enroll secret. Already-enrolled hosts are unaffected; new installers must be regenerated.
get/admin/endpoint-fleet/installersenrollment:manageList the stored per-OS fleetd installers and the pack version each was built with.
get/admin/endpoint-fleet/installers/:osenrollment:manageDownload the stored fleetd installer for an OS (pkg/msi/deb/rpm). Bakes the enroll secret.
put/admin/endpoint-fleet/installers/:osprovisioning:manageUpload a pre-built fleetd installer (provisioning lane / CI).
post/admin/endpoint-fleet/mdm-profileenrollment:manageDownload this deployment's macOS enrollment profile (.mobileconfig), the per-organization half of the agent installer. It carries the stored control-server address and enroll secret already used by the organization's machines. Re-downloading does not rotate the secret. The response body is a live credential. The mint goes to the endpoint-control service's org-scoped route. If the service's public URL moves, the next fetch re-mints against the new address and replaces the stored enrollment: previously issued profiles stop enrolling new machines, already-enrolled hosts keep their node keys. A transient failure never invalidates the stored enrollment.
get/admin/endpoint-fleet/mdm-serversconfig:readThe registered MDM server URLs (serverUrls, normalized https origins) used by legacy MDM tokens without proofMode: "macos-bootstrap". Content-free.
put/admin/endpoint-fleet/mdm-serversprovisioning:manageReplace the MDM server list used by the legacy endpoint-evidence lane ({"serverUrls": [...]}, up to 20, https only, no credentials or query). A bad entry answers 400 naming only its index. Audited as a count, then re-templated into the endpoint policy pack so the mdm-enrolled policy checks the new list.
get/admin/endpoint/orgobservability:readThe deployment's single endpoint-control org summary (host/online/stale counts, compliance, pack version). Requires ANYRAY_ENDPOINT_CONTROL_URL; every /admin/endpoint/* route answers 404 when the service is not configured. Also carries packSync, whether the endpoint-control service is checking machines against the same policy pack this gateway ships. Machines are evaluated against the pack baked into the service's image (it re-templates to that pack on every boot), so a deployment that updates the gateway without updating endpoint-control keeps reporting the older policies' verdicts, and restarting the service only re-applies the pack it already has. status is in-sync; stale, meaning the two versions differ; or unknown, meaning the org reports no pack version because it was templated before versions were recorded (never read unknown as drift). An unreachable service fails the whole read instead, so packSync is absent rather than unknown. A pack version is a content hash, so stale says the two halves disagree, not which one is behind.
get/admin/endpoint/hostsobservability:readContent-free host rows from the local service (first 100).
get/admin/endpoint/hosts/:idobservability:readPer-host policy results and script-run metadata. Org membership is proven before the host lookup.
get/admin/endpoint/policiesobservability:readThe org's policy set with enablement and pass/fail tallies.
get/admin/endpoint/policies/:name/hostsobservability:readThe reporting hosts currently failing one policy (the reverse drill of the host view).
post/admin/endpoint/policies/:nameenrollment:manageEnable or disable one policy ({"enabled": boolean}). Audited.
get/admin/endpoint/enroll-secretenrollment:managePresence only ({hasSecret}). The plaintext is stored hashed and cannot be re-read; rotation is the only way to obtain it.
post/admin/endpoint/enroll-secret/rotateenrollment:manageRotate the enroll secret. The plaintext is shown exactly once (cache-control: no-store); already-enrolled hosts are unaffected, new enrollments need regenerated installers/profiles. Audited.
get/admin/endpoint/evidence-connectorsenrollment:manageThe org's customer-managed evidence connectors and the connectable sources.
post/admin/endpoint/evidence-connectors/:source/rotateenrollment:manageMint or rotate the aer_ reporting token for one source. Shown exactly once (cache-control: no-store). Audited.
delete/admin/endpoint/evidence-connectors/:sourceenrollment:manageRevoke one source's reporting token; its reports stop being accepted immediately. Audited.
post/admin/endpoint/hosts/:id/remediateprovisioning:manageQueue an allowlisted remediation script on one org-owned host. Customer-managed evidence hosts return 409 monitor_only. Audited.
delete/admin/endpoint/hosts/:idprovisioning:manageDelete one org-owned host record (the machine re-appears if it enrolls again). Audited.
get/admin/authNoneAuth-mode probe (pre-session; reports whether SSO is required).
get/admin/auth/updateupdate:run + deployment ownerCapability probe (nginx auth_request) gating the console's updater trigger: 200 only when the pending update classifies soft, else 403. Applies nothing itself; the apply is POST /admin/update/run, proxied to the bundled updater.
Point liveness at /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 deliberately checks no dependency: 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.

Available from v1.10.224. The Helm chart wires both automatically and omits them on older images.

Spend & governance

The spend surface and per-user governance: token caps and USD budgets. In the OpenAPI spec and the explorer your gateway serves at GET /docs.

MethodPathCapabilityWhat it does
get/admin/spendconfig:readIn-memory spend summary: requests + tokens per attributed user.
get/admin/spend/dashboardconfig:readWindowed dashboard: money by billing class, trend, per-model/team/user/client splits, top strategies, latency, request success rate, prompt-cache economics. ?window=.
get/admin/spend/usageconfig:readPer-user usage detail: per-model & per-day breakdown, org per-model rollup, cap gauges. ?window=, ?limit=; optional ?from=/?to= (inclusive UTC days, YYYY-MM-DD) narrow every aggregate to that day range within the window, echoed back as range.
get/admin/spend/quality-parityconfig:readHoldout-vs-treated quality parity, additive tenant and pairedSession fields (including unpaired cost share, excluded-long-session share and error-only counts), plus measured observed-session gross-cost reduction against the 30% objective, with 95% CIs. ?window=, ?cohortKind= (a strategy id selects that strategy's per-strategy arm; omitted reads the whole-optimizer arm).
get/admin/spend-connectorsconfig:read + deployment ownerRead redacted Cursor, Devin, and GitHub Copilot connector state and poll status. Credentials are never returned.
put/admin/spend-connectorsproviderkeys:manage + deployment ownerPartially set, rotate, or remove write-only connector credentials. Omitted connectors stay unchanged; null or {} removes one.
post/admin/spend-connectors/syncproviderkeys:manage + deployment ownerSync vendor usage. Returns safe per-connector counts or skipped/failed.
get/admin/spend-connectors/usageconfig:read + deployment ownerAggregate vendor-native usage. ?days= accepts 1–90 and defaults to 30.
get/admin/spend/usage/propertiesconfig:readSpend grouped by one bounded custom attribution property (?name=, ?window=): values, counts, tokens, and USD only; the raw attribution map is never returned.
get/admin/auth-eventsconfig:readReason-coded auth/cap rejections (per-developer diagnostics) with remediation hints. ?user=, ?limit=.
get/admin/user-capsconfig:readPer-user monthly token caps and USD budgets + current-month token usage (usage) + per-user budget spend (budgetUsage) + cap-alert posture (webhook URL never returned).
put/admin/user-capsusercaps:writeSet caps, budgets, and cap-approach alert settings (thresholds + Slack-compatible webhook). Returns the same shape as the GET.
patch/admin/user-caps/:userusercaps:writeMerge a partial edit into one user's cap entry (omit = keep, null = clear). The automation lane for scoped admin API keys; never touches alert settings. Returns the same shape as the GET.

Spend connector rules

Connector credentials (cursor.adminApiKey, devin.serviceUserToken, and 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. Copilot's public list value is an estimate, not an invoice total.

These routes manage the whole deployment. 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 GET/PUT path uses the same encrypted store for older Cursor/Copilot clients. New clients should use /admin/spend-connectors.

Per-user caps and budgets

put /admin/user-caps replaces the whole caps map, so read the current config first and send it back with your edit. 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: an entry may govern tokens, dollars, both, or neither.

Because the map is replaced wholesale, two editors working from their own reads would overwrite each other. 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, so you can re-read and reapply. Omit it and the write is unconditional.

For a single user, prefer patch /admin/user-caps/:user: it edits one entry without re-sending the map (omitted fields are kept, null clears one), 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"].

Request
{
"caps": {
"dev-synthetic-1": { "monthlyTokens": 5000000 },
"dev-synthetic-2": { "maxUsd": 200, "softUsd": 150, "reset": "monthly" }
},
"alerts": { "thresholds": [0.8, 1] },
"expectedRevision": "9f2c1ab47e05d3purelysynthetic0000"
}

Both verbs answer with the same shape: the stored config (never the alert webhook URL, only webhookConfigured), each user's current-month token usage, and budgetUsage, the dollars spent in each budgeted user's own reset window. Users with no dollar budget are absent from budgetUsage.

Response
{
"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" }
}
}

Both limits are enforced on the inference endpoints and reject with HTTP 402 (user_token_cap_exceeded / user_budget_exceeded). The dollar figure is real out-of-pocket spend, not list price (an included subscription seat accrues only its over-allowance overage), and spend equal to maxUsd counts as over. The maxUsd ceiling only 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

MethodPathCapabilityWhat it does
get/admin/optimizer/settingsconfig:readRead the optimizer pipeline config.
put/admin/optimizer/settingsoptimizer:writeUpdate the optimizer pipeline config.
get/admin/optimizer/session-gateconfig:readPer-tenant strategy states and last paired-session evaluation.
put/admin/optimizer/session-gate/{kind}optimizer:writeSet or clear an operator gate override.
post/admin/optimizer/purgeoptimizer:purgePurge optimizer caches / session state.
get/admin/pricingconfig:readThe read-only official price table.
get/admin/model-aliasesconfig:readRead model-alias mappings.
put/admin/model-aliasesmodelaliases:writeReplace model-alias mappings. Send the revision you read (body field or If-Match) and a save built on a stale read is refused with 409 instead of overwriting it.
get/admin/routing-configconfig:readRead routing (single / loadbalance / fallback / conditional).
put/admin/routing-configrouting:writeUpdate routing config.
post/admin/test-requestplayground:runConsole playground: send a test request through the gateway.

Claude Desktop organization policy

MethodPathCapabilityWhat it does
get/admin/claude-desktop-policyconfig:read + deployment ownerRead the secret-free organization policy and resolved deployment fields. Personal OAuth grants are not enumerated. An absent row reads as a disabled empty policy.
put/admin/claude-desktop-policydesktopconfig:write + deployment ownerReplace {enabled, settings}. Accepts recognized Claude managed keys for models, connectors, plugins/skills, workspace/tool policy, egress, and application restrictions. Rejects server-owned inference/bootstrap fields, credential-shaped string values at every depth, headers/env, nested secret keys, unknown keys, and documents over 128 KiB.
get/admin/claude-desktop-policy/mobileconfigconfig:read + deployment ownerDownload the stable macOS trust/helper profile. Requires 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.
post/admin/claude-desktop-policy/connectors/probedesktopconfig:write + deployment ownerAsk the MCP server at {url} for its own name and tool list, so a connector can be added from its address. Answers ok (with name, version, tools, truncated), needs-auth when the server wants a sign-in first, or unreachable with a reason. HTTPS only; nothing is stored.
get/admin/claude-desktop-policy/connectors/readinessconfig:read + deployment ownerReturn the most-blocking organization sign-in state, /mcp/org endpoint URL, static served/dropped counts, cached access classification, and privacy-floored adoption. This read never probes connector vendors.
post/admin/claude-desktop-policy/connectors/readiness/refreshdesktopconfig:write + deployment ownerRefresh the process-local connector access cache with at most four probes in flight, then return readiness.
post/admin/claude-desktop-policy/connectors/authorizedesktopconfig:write + deployment ownerStart a personal OAuth flow. The result has only the vendor authorizationUrl; after consent, the grant belongs to the verified client-key user who redeems the short claim code.
get/connect/claude-desktop/bootstrapOptional client keySecret-free effective policy fetched by managed Claude Desktop clients. `?os=macos

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 machine-facing bootstrap route serves.

Providers

MethodPathCapabilityWhat it does
get/admin/provider-keysproviderkeys:manageList configured providers and named-key metadata (capabilities.keyIds).
get/admin/provider-capabilitiesconfig:readThe same list without the credentials, so a role that may route but not read a key can still populate its pickers.
put/admin/provider-keysproviderkeys:manageSet server-held provider API keys (per-slug patch; per-key form for named keys).

Access, SSO & enrollment

MethodPathCapabilityWhat it does
get/admin/idp-configconfig:readRead the SSO / IdP configuration.
get/admin/revoked-usersconfig:read + deployment ownerList revoked user emails.
post/admin/revoked-usersidp:manage + deployment ownerRevoke a user: kills their client keys and blocks re-enrollment (the offboarding step).
delete/admin/revoked-users/:emailidp:manage + deployment ownerUn-revoke, allowing re-enrollment.
get/admin/connect/directivesobservability:read + deployment ownerDirective history (every status, acked included), newest first. This is the coverage page's read. Optional ?user=<userHash> and ?limit=.
post/admin/connect/directivesidp:manage + deployment ownerQueue a per-user connect remediation directive (the console's "help / revert this developer" action); the machine pulls it through /connect/directive, within about two minutes on the desktop app, within 30 minutes on CLI-only machines. Delivery is pull-only and never pushed. kind is reapply_config, remint, revert, revert_tool, or update. 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; the desktop poll excludes it. See update limitations.
put/admin/idp-configidp:manageConfigure SSO / IdP.
delete/admin/idp-configidp:manageRemove the SSO / IdP configuration.
get/admin/scim/settingsconfig:read + deployment ownerRead redacted SCIM state, admin group, and group-to-team mappings. The bearer and hash are never returned.
put/admin/scim/settingsidp:manage + deployment ownerSet or rotate the SCIM bearer and update group mappings. Omitting bearerToken preserves it.
post/admin/workos-portal-linkidp:manageMint a WorkOS admin-portal link.
get/admin/sso/startNoneBegin the console SSO login handshake (pre-session).
get/admin/sso/callbackNoneSSO redirect callback (pre-session).
post/admin/sso/logoutNoneEnd the console SSO session.
get/admin/client-keysconfig:readList minted client keys (enrolled users).
post/admin/client-keysclientkeys:manageMint a client key.
delete/admin/client-keys/:idclientkeys:manageRevoke a client key.
get/admin/service-keysconfig:readList non-human service keys; tokens and hashes are never returned.
post/admin/service-keysclientkeys:manageMint an ark_svc_… key for an AI agent, CI job, or SDK script.
delete/admin/service-keys/:idclientkeys:manageRevoke a service key.
get/admin/api-keysconfig:read + deployment ownerList scoped admin API keys (aak_…). Metadata only; tokens and hashes are never returned.
post/admin/api-keysadminkeys:manage + deployment ownerMint a scoped admin API key with exactly one grant shape: a subset of the minter's capabilities or native v1 scopes. Scope-minted keys are v1-only; ungrantable scopes remain unavailable. A key cannot mint keys.
delete/admin/api-keys/:idadminkeys:manage + deployment ownerRevoke a scoped admin API key. A key cannot revoke keys.
get/admin/enrollment-linksconfig:readList enrollment / setup links.
post/admin/enrollment-linksenrollment:manageCreate an enrollment link.
delete/admin/enrollment-links/:idenrollment:manageDisable an enrollment link.
delete/admin/enrollment-links/:id/hardenrollment:manageHard-delete an enrollment link.
get/admin/provisioning-tokensconfig:readList deployment provisioning tokens.
post/admin/provisioning-tokensprovisioning:manageMint 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/:idprovisioning:manageRevoke a provisioning token.
post/admin/provisioning-tokens/:id/rotateprovisioning:manageRotate a provisioning token. A macOS bootstrap rotation returns a new one-time profile and certificate fingerprint.

Privacy, support & observability

MethodPathCapabilityWhat it does
get/admin/gdpr/users/:user/exportgdpr:manageExport a user's spend records.
delete/admin/gdpr/users/:usergdpr:manageErase a user's spend records (and cap-alert dedup rows).
get/admin/support/bundleconfig:readGenerate a support bundle for remote debugging.
post/admin/support/bundle/sharecontent:manageExplicitly rebuild, redact, and send a support bundle to the Billing app; return its receipt and audit metadata.
get/admin/observability/traces/:idobservability:readFetch one trace (content redacted unless observability:read-content).
get/admin/observability/*observability:readProxy to the observability read API for the console (lists, sessions, and traces/facets, the distinct model/provider/client ids behind the Traces filter dropdowns).
get/admin/connect-healthobservability:read + deployment ownerPer-seat connect-health rollup over the last 24h. Every successful verify or key-heartbeat updates durable presence; each row includes the pseudonymous userHash, last-seen instant, credential lane (verify or heartbeat), connect version (null identifies an old or unknown client), per-check statuses when reported, and a 0–100 compliance score against the fleet tool policy. Content-free: hashes, enum tokens, counts, and timestamps only. ?limit= caps seats (1–1000, default 200); ?user= narrows to one userHash.
get/admin/team-skillsconfig:readRead the team policy: shared skills, the fleet tool set (tools.enabled + per-tool tools.tiers), and the Connect update posture (updates.mode). When no tool policy has ever been saved, tools is absent and read-only toolsDefault lists what the deployment enrols instead.
put/admin/team-skillsteamskills:writeReplace the team policy. A legacy skills-only write preserves the stored tools, lanes and updates fields. Tiers: locked | required | recommended | optional (client default required). updates.mode: auto | directed | off (default auto, stored as absent).
get/admin/wizard-statusconfig:readOnboarding wizard progress.
put/admin/wizard-confirmwizard:writeConfirm an onboarding wizard step.

Service key request and response

post /admin/service-keys accepts a name and team, each 1–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 and an optional delegatedAttribution boolean (see AI agent enrollment). name becomes the key label and attributed service identity.

Request
{
"name": "synthetic-release-agent",
"team": "platform",
"monthlyBudgetUsd": 50
}

The 201 response returns the raw ark_svc_… value exactly once. The record is also returned by get /admin/service-keys inside { "keys": [...] }.

Response
{
"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"
}
}

Once the key has signed a verified request, the record also carries firstSeenAt, lastSeenAt (current to within a few minutes), and lastSeenTool, the client id of the last request: an inferred 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 that has not signed a request since these fields shipped has none of the three, which is how the console's Agents card tells "never connected" from "active". The same fields appear on every key from GET /admin/v1/keys.

The cap covers the key's successful request cost in the current UTC calendar month. Once completed spend is at or above the cap, inference returns HTTP 402 with error.code: "service_key_budget_exceeded". Enforcement is a soft cap: completed requests update the counter after the response, so concurrent in-flight requests can overshoot. Counter read/write failures pass traffic through so a budget-store outage does not stop agents or CI. Omitting the budget, or setting it to 0, leaves the key unlimited.

delete /admin/service-keys/:id returns { "ok": true }. The raw key cannot be recovered after minting; revoke it and mint a replacement.

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. The gateway is the protected resource and exposes the discovery endpoints clients need.

MethodPathAuthWhat it does
get/.well-known/oauth-protected-resourcepublicReturn the exact https://<gateway>/mcp/org resource, the managed AuthKit issuer, and bearer-header support. Cache-Control: no-store; bursts can return 429 with Retry-After.
get/.well-known/oauth-authorization-serverpublicCompatibility 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. Cache-Control: no-store; bursts can return 429 with Retry-After.
post · delete/mcp/orgclient key or AuthKit bearerThe gateway serves the organization's remote connectors under the verified caller's identity. Authless tools are shared. When the runtime switch is on, one anyray_connect tool appears if at least one OAuth connector needs this caller to sign in. Once a grant exists, the connector's anyray_oauth__<connector>__<tool> tools replace it. DELETE acknowledges session closure with 204.
get/mcp/orgclient key or AuthKit bearerAuthenticate, then return 405 with Allow: POST, DELETE.

anyray_connect accepts three call shapes. With no arguments it lists the connectors waiting for the caller and starts nothing. { "connector": "Atlassian" } returns an authorization URL. The vendor callback shows a single-use, five-minute claim code. Redeem it with { "connector": "Atlassian", "claim": "ABCDEFGHJKLM" }; the existing claim path 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.

The tool is off by default. An admin enables it in the Postgres-backed runtime settings with { "mcpConnectTool": true } through PUT /admin/settings or PUT /admin/v1/settings. A remembered OAuth tool called without a usable grant returns HTTP 200 with result.isError: true. It names the connector and points to anyray_connect only while the switch is on. 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.

A presented x-anyray-api-key, or Authorization: Bearer ark_..., takes precedence over the AuthKit lane. The AuthKit token must use RS256, the configured issuer, a scalar audience exactly equal to this gateway's /mcp/org URL, valid sub/iat/exp claims, and a non-empty org_id. No custom mcp:org scope is used.

After local JWT verification, the gateway sends only sub and org_id to Billing under the deployment bearer. Billing requires an activated and currently connected SSO configuration plus a live matching WorkOS organization membership. It applies the tenant's group-to-team map, allowed domains, and revocation state. The AuthKit bearer never reaches Billing. Identity throttles return 429 with Retry-After; a cheaper IP/process guard applies before JWT verification and discovery. Concurrent config, JWKS, and metadata refreshes share one outbound request. Identity or discovery outages return 503. An invalid token returns 401 with the OAuth challenge; a verified subject who is not a live member returns 403 without one, because re-challenging only repeats a login that already succeeded. Keep non-SSO methods disabled in the WorkOS domain policy and require SSO in the organization policy so Google or Magic Auth cannot satisfy the login; that hardening is recommended and does not gate the lane, since every request re-checks membership, connection state, domain policy and revocation. The Billing bridge returns 404, naming the cause, until Anyray registers the exact Resource Indicator derived from the deployment's current public gateway URL. Changing the organization or URL reopens that task. The readiness path is passive: it never creates a WorkOS organization or SSO connection for an ordinary tenant. That happens only after an authenticated owner explicitly chooses Set up SSO, or an operator deliberately binds an existing WorkOS organization. Non-SSO tenants keep using client-key MCP auth.

/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. 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, not by end apps. Carries savings beacons, policy, and CCR retrieve/recall. No admin capability; authenticated as the connected client.

MethodPathWhat it does
post/connect/verifyVerify a client key / enrollment.
post/connect/mcp-oauth/authorizeStart an OAuth browser flow. Body { "name": "connector name", "loopbackPort": 49152, "loopbackNonce": "<43 base64url chars>" }. The two loopback fields are optional but go together: an integer 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, or anything else, answers 400. Returns { "authorizationUrl": "https://..." }, plus loopbackPort echoed back when it was accepted, so Connect can tell a gateway that predates the field (that one drops it and shows the code in the browser). Starting the flow does not bind a user. After vendor consent the gateway's 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 and an empty body. Connect redeems the code only under its own nonce digest, so a code from another run pushed at that port is ignored.
post/connect/mcp-oauth/completeRedeem the short code displayed in the consenting browser. Body { "name": "connector name", "claimCode": "12-character code" }; returns { "status": "ok", "user": "verified account" }. The grant binds to the verified client-key account redeeming the single-use, five-minute code.
post/connect/endpoint-enrollmentReturn the organization's end-point fleet URL and enroll secret so an authenticated seat can configure the shared, credential-free machine agent. Client-key gated; the response contains a live credential and must not be logged or rendered.
post/connect/key/heartbeatExtend the existing enrollment key without replacing it, including recovery after its ordinary bearer expiry. Requires the current ark_ key in x-anyray-api-key, the Billing app-signed DevCert, and a fresh purpose: "key-heartbeat" proof from its bound Ed25519 or device P-256 key; the bearer alone is insufficient and the response never returns a replacement key.
get/connect/policyFetch the client's effective policy: shared skills (the console's Skills and Guidance plus any legacy team skills), routing origin, bound team, and the fleet tool set (tools.enabled + optional per-tool tools.tiers). A deployment that has never saved a tool policy is served the default set with tools.defaulted: true, so a tool a developer installs after enrollment still gets configured; a seat whose developer named their own tools with --tools ignores a defaulted list, and an admin's saved list always wins. skillsIncomplete: true means a skills store could not be read this call, so clients write what is listed and remove nothing. mcp.org.url is the organization MCP endpoint when the gateway has a valid public HTTPS origin; Connect writes it into every managed tool. Optional ?model=<id> also returns modelContext.contextTokens, that model's real context window, so a client can budget against the true window instead of its own default. Omitted when the model is unknown.
get/connect/savingsReport realized savings for the client.
post/connect/hook-savingsRecord hook-trim savings (PostToolUse).
post/connect/fleet-policy-webhookCompatibility receiver for a self-hosted Fleet server selected by a stored 0054 config. It verifies the stored secret and drives Fleet's run-script API with a per-host cooldown. endpoint-control orgs do not use this route because their leased, bounded remediation loop runs server-side. An absent or unreadable Fleet config answers 503 and never silently succeeds.
post/connect/machineReport the machine↔seat join for the endpoint lane. Gated like /connect/key/heartbeat (current ark_ key + a fresh device possession proof), with machineUuid as a sibling of the proof (a legacy MDM cert also signs it inside the challenge, and that lane's managed-device check reads only the signed copy). Only sha256 hashes are stored, never the raw UUID, a hostname, or a username. Off-Postgres the join lane is inert (recorded: false).
get/connect/directivePull the seat's next remediation directive ({directive:{id,kind,tool?}} or {}), the heartbeat-era counterpart of the verify-lane piggyback. Key-gated; scoped to the caller's own pseudonymous seat; a directive re-delivers until acked. Supported kinds are reapply_config, remint, revert, revert_tool, and update. 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 directive); it stays queued and lands once the client updates.
post/connect/directive/ackAcknowledge an applied directive ({id, result:"ok"|"failed"}). Scoped to the caller's own seat, so a leaked id can't close another developer's directive. A revert that reports ok also marks the seat offboarded; the kind is read from the stored directive, never from the payload.
post/connect/offboardAnnounce that this seat has been reverted off Anyray. The last call a full anyray-connect --revert makes, sent while the credential it is about to delete still authenticates. Empty body, scoped to the caller's own seat. It marks the seat offboarded so the console can tell a deliberate disconnect from a machine that stopped reporting, and so the seat stops scoring as failing; re-enrolling clears the mark. Always 200 with {status:"ok", recorded:<bool>}; recorded:false means the durable store could not hold it, which is never an error here because the developer's revert does not depend on this call.
post/connect/optimize-outputOptimize a tool output at the source.
post/mcpThe gateway as a remote MCP server (streamable HTTP): serves anyray_retrieve + anyray_recall to any MCP-capable agent runtime, client-key gated. Every valid authenticated JSON-RPC message renews the key's 6-hour retrieval lease; GET /mcp answers 405.
post/connect/recall · post /connect/retrieveCCR recall / retrieve of externalized context. A human/CLI caller sends x-anyray-retrieve-probe: 1 (the anyray-connect retrieve / recall verbs do) so the call does not latch retrieve capability; only the MCP server's calls prove the model can pull spans back.
post/connect/mcp-heartbeatMCP-server liveness heartbeat. Connect sends it after initialization and every 5 minutes while the stdio transport is alive; each accepted call renews a 6-hour retrieval lease, which authorizes the deferral_latched evidence class when the tool is hidden behind deferred loading. A request that declares anyray_retrieve outright never needs it.
post/connect/healthRecord the results of anyray-connect doctor checks (gateway reachability, retrieval loop, per-tool MCP registration, enrollment, connect run) plus the reporting client's own version. Best-effort: always 200; the gateway forwards a rolling 24h rollup to the Billing app on the next meter round.
post/connect/device-tokenRegister a short-lived device-page token. Body { "token": "<43-char base64url>", "ttlSeconds": <n> }; the TTL is clamped to 300–3900s, only the token's sha256 is stored (keyed to the caller's pseudonymous seat hash), and a new registration revokes the seat's previous token. 503 (static message) on a deployment without the shared Postgres store.
delete/connect/device-tokenRevoke the caller's seat's device token so it stops verifying immediately instead of aging out with its TTL. Current anyray-connect does not call this: a revert deletes the token's local file and lets the row expire on its own, because the row configures nothing on the machine. The route stays for clients that predate that change and for revoking a token by hand. Idempotent: 200 whether or not a token was registered; 503 (static message) on a deployment without the shared Postgres store.

The anyray_connect tool inside /mcp/org uses these same authorization and claim operations. OAuth state remains 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. This observation requires an existing valid client key; identity comes from that key. The response adds loginObservationRecorded, while legacy reports retain their existing response. /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/optimize-output accepts retrievalContext: "available" | "unavailable" | "unknown". Connect sends it from the current hook consumer, not from a long-lived client identity. A reversible: true explicit anyray_read is optimized only when the value is available; unavailable, unknown, an invalid value, or an absent field returns a confirmed byte-identical no-op (handled: true). Generic Bash/Grep output keeps the existing handle-free terminal-trim behavior for every value. This makes old Connect → new gateway fail closed; new Connect also skips the request locally when an explicit read cannot retrieve.

The device page: /device

GET /device serves the developer's own device page (what the tray's "Open my device page" and anyray-connect device-url open) and GET /device/health?dt=<token> backs it. Both are gated by the short-lived device token alone, with no admin capability and no client key: Connect mints the 256-bit token and registers it above. device-url creates a replacement on demand, and scheduled renewal also replaces it, so the URL itself is the short-lived capability. An unknown, expired, or absent token gets a static 401; a valid one returns exactly that seat's health rollup (opaque userHash, per-check statuses, compliance score, the same fields as one /admin/connect-health row). Served by the gateway (not the console): the connect CLI knows its gateway origin, and needs no second origin to discover.

Not a public API

/public/* (console login: /public/auth, /public/auth/session, /public/logs) and /log/stream back the console UI's in-browser session. They aren't part of the integration surface; for automation use /admin/* with a scoped admin API key (preferred, 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.