Skip to main content

API reference

The gateway's inference, organization MCP, admin, and inbound SCIM v2 surfaces.

Browse and try every endpoint, inference (/v1/*) and admin (/admin/*), in the interactive API explorer your gateway serves itself at GET /docs, backed by the OpenAPI 3.1 spec at GET /openapi.json (load that into Postman or codegen too). Both routes are unauthenticated and expose only the API shape; "try it out" calls run against the same gateway, so real requests still need your client key or admin token. A copy of the spec is published at docs.anyray.ai/openapi.json if you don't have a deployment in front of you. The capability-annotated index of every route is the Endpoint reference.

Open the explorer
open http://localhost:8787/docs # or https://<your-anyray-gateway>/docs

Base URL

The gateway is self-hosted: it runs on your own infrastructure (default port 8787), not on docs.anyray.ai. Put it behind TLS for any networked deployment.

Base URL
https://<your-anyray-gateway> # e.g. http://localhost:8787 in-network

Deploying and configuring the gateway (provider keys, content mode, the optimizer URL): Configure.

Authentication

Send your Anyray client key as a bearer token, in x-anyray-api-key, or, for Anthropic-native clients on /v1/messages, in x-api-key. The real provider key stays server-side.

Authenticated request
curl https://<your-anyray-gateway>/v1/chat/completions \
-H "Authorization: Bearer $ANYRAY_API_KEY" \
-H "Content-Type: application/json" \
-H 'x-anyray-provider: openai' \
-d '{
"model": "gpt-4o",
"messages": [{ "role": "user", "content": "Summarize the release notes." }]
}'

Six credentials:

CredentialUsed onGrants
Client key (Authorization: Bearer, x-anyray-api-key, or ark_ in x-api-key)/v1/* inferenceThe holder's user/team attribution. Minted by an admin enrollment flow or self-service SSO with anyray-connect --sso.
Service key (ark_svc_…, sent through the same headers)/v1/* inferenceA revocable non-human service identity for an AI agent, CI job, or SDK script, with an optional per-key monthly dollar budget. No interactive SSO.
AuthKit access token (Authorization: Bearer)/mcp/orgA WorkOS user in the customer organization. The audience must equal this gateway's exact /mcp/org URL. The gateway verifies it and Billing checks live membership.
Admin token (ANYRAY_ADMIN_TOKEN)The console + /admin/* config APIBreak-glass owner access, kept server-side. Each /admin/* route requires a capability; enterprises can add console SSO + RBAC.
Admin API key (aak_…, Authorization: Bearer)/admin/* config APIA revocable automation credential carrying either an explicit capability subset or native Management API v1 scopes. Never inference; never trace content. See Scoped admin API keys.
SCIM bearer (Authorization: Bearer)/scim/v2/*Lets the configured IdP provision users and groups for one tenant. Only its SHA-256 hash is stored.
Inference uses a minted client key

Every /v1/* request needs a valid minted key. The key's user and team control attribution; x-anyray-metadata cannot override them. Once the organization configures SSO, a developer mints their own key with the enrollment command from Users → SSO enrollment; an org admin can also mint and rotate keys. OAuth/OIDC never runs on /v1/* itself.

Missing, unknown, expired, revoked, and SCIM-deactivated identities return HTTP 401 with missing_key, key_unknown, key_expired, key_revoked, or user_deactivated and a safe fix hint. An unreachable key store is not a 401: verification fails closed with HTTP 503 and key_store_unavailable, so a database outage is never mistaken for a bad credential.

Organization MCP access with SSO

An MCP client that cannot attach a static header adds one URL:

https://<your-anyray-gateway>/mcp/org

The unauthenticated request returns 401 and points the client to the protected-resource metadata:

WWW-Authenticate: Bearer resource_metadata="https://<your-anyray-gateway>/.well-known/oauth-protected-resource"

That public metadata names WorkOS AuthKit as the authorization server. AuthKit runs the authorization flow through the enterprise SSO connection already attached to the customer's WorkOS organization. The gateway does not implement authorization codes, PKCE, client registration, consent, token refresh, or revocation.

The access token must meet all of these checks:

Claim or propertyRequired value
signing algorithmRS256
issthe managed AuthKit issuer
audone scalar string equal to https://<your-anyray-gateway>/mcp/org
time and subjectvalid sub, iat, and exp
organizationnon-empty WorkOS org_id

No custom mcp:org scope is advertised or required. The exact resource audience separates one gateway from another.

After JWT verification, the gateway sends only sub and org_id to Billing under its deployment bearer. Billing requires the tenant's activated SSO marker and a currently active WorkOS connection. It fetches the current WorkOS user, requires exactly one active membership in the configured tenant organization, resolves the current groups to an Anyray team, and applies allowed domains and revocation. The AuthKit token never reaches Billing or storage. Keep non-SSO methods disabled in the WorkOS domain policy and require SSO in the organization policy. Otherwise a manually enabled Google or Magic Auth method can bypass the enterprise IdP.

A client key still works on /mcp/org and takes precedence when presented. This keeps Connect-managed tools compatible. Pre-verification and live identity throttles return 429 with Retry-After; concurrent config, JWKS, and metadata refreshes share one outbound request. Billing, WorkOS, or AuthKit key-discovery outages fail closed with 503. See Organization MCP hub for setup and the endpoint reference for every route.

Billing also returns 503 until a vendor operator confirms the current WorkOS organization policy and the exact Resource Indicator derived from this deployment's public gateway URL. A WorkOS organization rebind or gateway URL change invalidates the matching confirmation. This lane never provisions WorkOS for an ordinary tenant. The WorkOS organization is created only after an authenticated owner explicitly chooses Set up SSO, or an operator deliberately binds an existing organization. Non-SSO tenants continue to use client-key MCP authentication.

Service keys for agents and CI

Three admin routes manage service keys:

MethodPathCapabilityShape
post/admin/service-keysclientkeys:manage{ "name": string, "team": string, "monthlyBudgetUsd"?: number }201 { "key": "ark_svc_…", "record": {...} }
get/admin/service-keysconfig:read{ "keys": [...] }; filtered to service keys, with source and type set to service
delete/admin/service-keys/:idclientkeys:manageRevokes the key and returns { "ok": true }
Mint a budgeted CI key
curl https://<your-anyray-gateway>/admin/service-keys \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "synthetic-ci-release",
"team": "platform",
"monthlyBudgetUsd": 25
}'

Store the returned key immediately; the gateway stores only its SHA-256 hash and never returns the raw token again. A budget counts successful request cost per UTC calendar month. At or above the budget, covered inference routes return:

HTTP 402
{
"error": {
"code": "service_key_budget_exceeded",
"message": "This service key has reached its monthly budget. Ask an administrator to raise the budget or wait for the next UTC month."
}
}

The cap is soft and fails open: completed requests update the shared counter after their responses, so concurrent requests can overshoot, and a counter outage lets requests proceed. A key without monthlyBudgetUsd, or with a value of 0, is unlimited.

Scoped admin API keys

An admin API key (aak_…) scripts the /admin/* surface without the break-glass ANYRAY_ADMIN_TOKEN. Mint with exactly one grant shape: an explicit subset of the admin capabilities, or native Management API v1 scopes. The grant is fixed at mint time and can never exceed the minter's own access; content, content-mode, SSO config, self-update, and key management remain ungrantable. Keys expire after 90 days by default; pass expiresInDays for a different horizon or neverExpires: true to opt out explicitly. Minting requires the adminkeys:manage capability (security_admin and up).

A scope-minted key is deliberately v1-only. It has no capabilities, so every legacy /admin/* capability gate rejects it; use it with /admin/v1/* (and the scope-less GET /admin/v1/me). A v1 403 names the exact scope to mint in required_scope.

MethodPathCapabilityShape
post/admin/api-keysadminkeys:manageExactly one of caps: string[] or scopes: string[], plus optional label and expiry → 201 { "key": "aak_…", "record": {...} }
get/admin/api-keysconfig:read{ "keys": [...] }; metadata only, hashes stripped
delete/admin/api-keys/:idadminkeys:manageRevokes the key and returns { "ok": true }

Store the returned key immediately; only its SHA-256 hash is kept. Revocation takes effect on the next request, on every replica. Admin writes made with a key are attributed in the audit log as apikey:<id>.

For a CI dashboard, mint the one scope its read needs, then call the v1 dashboard:

Mint a CI dashboard key by scope
curl -X POST https://<your-anyray-gateway>/admin/v1/admin-keys \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "ci-dashboard",
"scopes": ["spend:read"]
}'
Read the spend dashboard with the scope-minted key
curl https://<your-anyray-gateway>/admin/v1/spend/dashboard \
-H "Authorization: Bearer $ADMIN_API_KEY"

On the legacy surface a budget bot needs a capability grant instead: a key that can read config and write per-user caps.

Mint a budget-bot key (as an admin)
curl https://<your-anyray-gateway>/admin/api-keys \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "budget-bot",
"caps": ["config:read", "usercaps:write"]
}'
Extend one user's budget (with the key)
curl -X PATCH https://<your-anyray-gateway>/admin/user-caps/alice \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxUsd": 250 }'

PATCH /admin/user-caps/:user (capability usercaps:write) merges a partial edit into one user's entry: a field you omit is kept, null clears it, a value replaces it. Accepted fields are monthlyTokens, maxUsd, softUsd, and reset (never | daily | weekly | monthly), plus an optional expectedRevision for a conditional write (a stale revision returns 409). Unknown fields are rejected with 400. The whole-map PUT /admin/user-caps remains for bulk edits.

Use keys against the gateway origin directly

The console host's proxy replaces the Authorization header with the browser login cookie, so an aak_ key only works against the gateway itself (port 8787 by default), not through the console hostname.

If the key store is unreachable, aak_ verification fails closed with HTTP 503 and admin_key_store_unavailable (never a 401), and the break-glass admin token keeps working.

Management API v1

/admin/v1/* is the /admin/* surface re-cut for scripts and API keys: plural-noun resources, one permission rule you can read off the URL, one error envelope, and a request id on every response. The surface is stable (x-anyray-api-status: stable): response shapes are the documented contract, and breaking changes would arrive only as /admin/v2. Legacy /admin/* data routes still answer, now with Deprecation: true and a Link: <successor>; rel="successor-version" header naming their v1 path; plan their retirement into your scripts. Requests are capped at 300 per minute per principal (429 + Retry-After past it).

Beyond the re-served routes, v1 adds a key lifecycle (GET/PATCH /admin/v1/keys/{id}, POST /admin/v1/keys/{id}/rotate returning the fresh token exactly once), per-provider key writes (PUT/DELETE /admin/v1/providers/{slug}/key), and the audit trail (GET /admin/v1/audit/events?limit=&cursor=&category=, a { "data": [...], "next_cursor": ... } page over the admin/GDPR trail, audit:read). Self-service for developers lives on the inference plane instead: GET /v1/me, /v1/me/usage, and /v1/me/limits answer with the verified key holder's identity, month-to-date usage, and the ceilings that would 402 them; no admin credential involved.

The scope rule

A scope is <resource>:<action>, where the resource is the first path segment after /admin/v1/:

RequestScope it needs
GET /admin/v1/<resource>/…<resource>:read
POST, PUT, PATCH, DELETE /admin/v1/<resource>/…<resource>:write
POST /admin/v1/<resource>/…/query (a read whose filter needs a body)<resource>:read

Resources: keys, admin-keys, users, spend, providers, routing, optimizer, policies, settings, sso, enrollment, provisioning, fleet, traces, audit, playground. GET /admin/v1/me needs no scope and returns the scopes the caller holds. The named exceptions, each declared on its operation in the OpenAPI spec as x-required-scope:

ScopeGatesGrantable to a key
users:revokeRevoking or reinstating a developerno
users:gdprA user's export and eraseyes
providers:write on GET /admin/v1/providersA credential-grade read: provider keys come back in full, so possession is the privilege, not the verbyes
fleet:enrollEnrolling machines: the enroll secret (read and rotate), evidence-connector bearers, installer download, the MDM profile, enabling a policyyes
fleet:remediateRunning a remediation script, retiring a host, pushing policyyes
playground:runThe admin playgroundyes
fleet:directQueuing a directive for a developer seat (revert, re-mint, update)no
settings:contentContent mode and trace retention: PUT /admin/v1/settings/content, and any PUT /admin/v1/settings body naming a field outside the runtime-settings allowlistno
settings:updateThe self-update policyno
sso:writeIdP and SCIM configurationno
admin-keys:writeMinting and revoking aak_ keysno
traces:content, fleet:contentPrompt/response bodies; remediation script outputno

Existing grants keep their reach: a role or an aak_ key still carries capabilities, and each capability translates to the scopes it gated on the legacy paths (config:read is every <resource>:read except the audit, trace, and fleet lanes; usercaps:write is users:write; enrollment:manage is enrollment:write plus fleet:enroll; provisioning:manage is provisioning:write, fleet:write, and fleet:remediate; and so on). The legacy trust boundaries between enrolling, provisioning, and directing never collapse into one another.

The deliberate differences, each pinned by a test:

  • The audit lane (/admin/v1/audit/*) needs audit:read, which config:read no longer implies.
  • The fleet listings (including GET …/mdm-servers, GET …/installers, and GET …/evidence-connectors) are fleet:read.
  • Single-route capabilities of one tier fold into one write scope: routing:write covers routing config and aliases, policies:write covers team skills and the Claude Desktop policy, optimizer:write covers settings and purge.
  • The two non-content runtime settings (seatOverflow, seatOverflowHourlyMaxRequests) are settings:write; every other field of PUT /admin/v1/settings (content mode, trace retention, the heartbeat tier, storeCapacityBytes, and anything added later) stays behind settings:content.

Tenant and deployment resources

Each operation carries x-scope: tenant or x-scope: deployment. In fleet mode (ANYRAY_MULTI_TENANT=true) a tenant-bound principal (an SSO operator, or a key minted for one tenant) reaches tenant resources only (keys, spend, optimizer, traces, the GDPR routes, health); a deployment resource answers 403 with deployment_scope_required. Off fleet mode the level is informational.

Errors

Every error, whatever the status, uses one envelope:

Management API error envelope
{
"error": {
"code": "insufficient_scope",
"message": "This operation requires the 'audit:read' scope, which this API key was not granted.",
"required_scope": "audit:read",
"request_id": "8c2f0b1e-synthetic-request-id"
}
}

code is stable: unauthenticated, insufficient_scope, deployment_scope_required, invalid_request, not_found, conflict, rate_limited, unavailable, plus the legacy reason codes such as admin_key_store_unavailable. request_id repeats the x-request-id response header; send your own identifier in that request header and it is echoed back. 401 means the credential is missing or bad; 403 means it is fine but lacks the scope, and names it.

Extend one user's budget with a scoped key, on v1
curl -X PATCH https://<your-anyray-gateway>/admin/v1/users/alice/limits \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "maxUsd": 250 }'

The resource-by-resource map, with the legacy twin of every v1 path, is on the endpoint reference. Every v1 operation in the spec also names its twin in x-legacy-operation.

Request headers

Beyond Authorization / Content-Type, the gateway reads a few x-anyray-* headers:

x-anyray-providerstring

Provider to route to: openai, anthropic, vertex-ai, bedrock, azure-openai, … Optional when a default provider or routing config is set server-side.

x-anyray-metadatastring (JSON)

Attribution, e.g. {"user":"alice","team":"platform"}. Drives spend attribution and per-user caps. Never carries prompt/response content.

x-anyray-configstring (JSON)

Per-request routing: fallback list, load-balance weights, retry policy. No credentials; they resolve from the server-held key store.

Response headers

Every response carries x-anyray-provider (the provider the request was routed to) and x-anyray-trace-id. When the provider returns a server error (any 5xx, such as an Anthropic 529 overloaded_error), the gateway also stamps the markers below. The status and the error type are forwarded unchanged; only the human-readable error.message gains a sentence naming the provider that produced it:

Relayed Anthropic 529
{
"type": "error",
"error": {
"type": "overloaded_error",
"message": "Overloaded — this error was returned by Anthropic (api.anthropic.com) and relayed unchanged by the Anyray gateway; it is not an Anyray gateway failure. Check https://status.claude.ai/ for provider status."
}
}
x-anyray-error-sourcestring

Present and set to upstream when the error originated at the provider, not the gateway. Absent on success and on gateway-generated errors.

x-anyray-upstream-providerstring

The upstream provider that returned the error: anthropic, openai, vertex-ai, … (omitted when the provider is unknown).

x-anyray-upstream-statusstring

The provider's own HTTP status (e.g. 529, 503).

Telling provider errors from gateway errors

No x-anyray-error-source: upstream header on a 5xx means the gateway itself failed, so check your deployment. With it, the fault is on the provider's side (capacity/overload); retrying shortly usually clears it.

Endpoints

/v1/* serves an OpenAI-compatible inference API plus the Anthropic-native Messages API; /admin/* is the admin-gated configuration and reporting API; /sso/cli/* is the pre-key self-service login handshake used by Connect; /scim/v2/* is an inbound SCIM 2.0 server. The Endpoint reference is the complete, capability-annotated index of every route; full request/response schemas live in the OpenAPI spec. The optimizer's own before/after-request hooks are the separate optimizer protocol.

Retrieval via MCP: POST /mcp

The gateway is a remote MCP server (streamable HTTP). It serves the retrieval pair, anyray_retrieve and anyray_recall, behind the same client key as every other route, so an MCP-capable agent runtime closes the retrieval loop with a single server entry and no code:

Any MCP client registration
{
"type": "http",
"url": "https://<your-anyray-gateway>/mcp",
"headers": { "x-anyray-api-key": "<client key>" }
}

The path is /mcp, not /v1/mcp. /v1/* is the inference proxy, and it catches every path it does not recognize, so /v1/mcp is sent upstream as an inference request and comes back 500 rather than 404.

Registering the server puts the two tools in every request the client sends. The gateway forwards them to the model only from the first request of a session that carries a retrieve ctx_ marker, and on every request after it. Until then the model sees the same tool list it would without the server: the two definitions on their own changed how an agent worked a lint task, before anything had been shortened, and a session that never shortens an output has no use for them. The lane still counts as retrieve-capable from the first request, so the reversible strategies run as before.

Hosted connectors (Anthropic mcp_servers, OpenAI Responses type:"mcp") may point here and are credited as retrieve evidence when they target this gateway's own /mcp. First-party provider APIs only (not Bedrock/Vertex), and the endpoint must be reachable from the provider's cloud.

One JSON-RPC 2.0 message per POST (initialize, ping, tools/list, tools/call); notifications answer 202 with no body; batch arrays are rejected; GET /mcp is 405 (no server-initiated stream). There is no session id; every call authenticates independently. Every valid message renews the presenting key's 6-hour retrieval lease, the same evidence the stdio server's heartbeat provides, so retrieval-dependent optimization unlocks after initialization. Tool-level failures come back in-band (isError: true with "unknown or expired handle" or "retrieval unavailable"), never as protocol errors, so the model can read them and adapt.

Retrieval: POST /connect/retrieve

An agent you build yourself has to be given a read path, or optimization runs one-way. Declare a tool named anyray_retrieve and implement it against this route with the same client key you already send. These are three independent requirements:

  • The declaration proves the model can select the tool on this turn. It matches on the tool name (anyray_retrieve, or a namespaced mcp__…__anyray_retrieve), so renaming the tool silently turns retrieval off.
  • The implementation is what resolves a handle once the model calls it.
  • The liveness signal proves that implementation is running. Send an authenticated empty POST /connect/mcp-heartbeat at worker start and every 5 minutes while it can execute tools.

Without both a callable path and a fresh live lease, the gateway keeps clientCanRetrieve false. Retrieval-dependent strategies stand down, and degradable strategies keep their handle-free mode.

Resolve a ctx_ handle
curl -X POST https://<your-anyray-gateway>/connect/retrieve \
-H "x-anyray-api-key: $ANYRAY_CLIENT_KEY" \
-H "Content-Type: application/json" \
-d '{"handle":"ctx_abc123"}'
handlestringrequired

The ctx_… handle from an [anyray: … retrieve ctx_…] marker in the message the model received.

startLine / endLinenumber

Read back a slice instead of the whole original.

grepstring

Return matching lines with context. Max 256 characters.

Answers {"status":"ok","content":"…","handle":"ctx_…"}. An unknown or expired handle is 404, an unavailable retrieval store is 503, and a missing or invalid key is 401. This is the route the /mcp endpoint's anyray_retrieve tool calls into; implement against it directly only when the runtime cannot register an MCP server. POST /connect/recall takes a natural-language query instead of a handle and returns ranked handle + score + preview matches, for when the marker has scrolled out of the model's view.

A human or CLI caller that is only probing sends x-anyray-retrieve-probe: 1 so the call does not latch capability; only the model's own calls prove it can pull spans back. Worked examples per SDK: Retrieval reference.

Claude Desktop managed bootstrap

An organization that routes Claude Desktop through Anyray manages its third-party profile through the deployment-owner API:

Set the organization policy
curl -X PUT https://<your-anyray-gateway>/admin/claude-desktop-policy \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"settings": {
"inferenceModels": [{"name": "claude-sonnet-4-6"}],
"managedMcpServers": {},
"allowedPluginMarketplaces": [],
"chatTabEnabled": true,
"coworkEgressAllowedHosts": ["*"]
}
}'

GET /connect/claude-desktop/bootstrap?os=macos|windows|linux returns the effective secret-free policy, with the gateway provider/base URL, helper credential mode, and that OS's inferenceCredentialHelper path injected by the server (os defaults to macos). Current Claude Desktop sends Authorization: Bearer <client key> on this fetch through the MDM-pinned bootstrapHeadersHelper; the route verifies a presented bearer (401 when rejected) and still serves an unauthenticated fetch in the no-OIDC lane. It returns 404 while disabled. The write boundary rejects credential-shaped string values at every depth, including values placed under otherwise allowed free-text keys. The authenticated admin routes remain available in headless gateway deployments. GET /admin/claude-desktop-policy/mobileconfig downloads the MDM trust profile, which pins that endpoint and /usr/local/bin/anyray-bootstrap-headers-helper without embedding a key; the response names /usr/local/bin/anyray-credential-helper. POST /admin/claude-desktop-policy/connectors/probe takes {"url": "https://..."} and asks that MCP server for its own name and tool list, so a connector row can be filled in from its address: {"status": "ok", "name", "version", "tools": [...], "truncated"} when it answers openly, {"status": "needs-auth", ...} when it wants a sign-in first, and {"status": "unreachable", "reason"} otherwise. HTTPS only, and nothing is stored.

POST /admin/claude-desktop-policy/connectors/sync waits for one connector-mirror round and returns {mcpHub} with content-free status, counts, and recent timing outcomes. The tool count covers shared authless tools only. A total round failure returns lastSyncCounts: null. Its v1 twin is POST /admin/v1/policies/claude-desktop/connectors/sync (policies:write).

Import and fleet deployment: Desktop apps.

POST /admin/claude-desktop-policy/connectors/authorize takes {"name": "Atlassian"} and returns a vendor authorizationUrl. After consent, the callback page shows a short claim code. Redeem it with POST /connect/mcp-oauth/complete and a client key. The grant belongs to the verified account that redeems the code, never the account that started the flow. Developers start their own flow with POST /connect/mcp-oauth/authorize. Neither response contains a token.

POST /admin/claude-desktop-policy/connectors/revoke takes {"name": "Atlassian"} and requires desktopconfig:write (policies:write on /admin/v1/policies/claude-desktop/connectors/revoke). It returns {"state":"needs-auth","outcome":"hub-grant-removed"} only after the hub has removed its stored grant. It does not claim the upstream provider session was revoked.

Not every endpoint runs on every provider

Chat completions, embeddings, and the Anthropic Messages API work across all supported providers. Image, audio, Responses, file, and Batch endpoints are OpenAI / Azure-only passthrough (audio also works on groq).

SCIM v2 provisioning

Point the IdP at this base URL and send the separately configured SCIM bearer on every request:

SCIM base URL
https://<your-anyray-gateway>/scim/v2
ResourceRoutes
DiscoveryGET /ServiceProviderConfig, GET /ResourceTypes, GET /Schemas
UsersPOST /Users, GET /Users, GET /Users/:id, PUT /Users/:id, PATCH /Users/:id, DELETE /Users/:id
GroupsPOST /Groups, GET /Groups, GET /Groups/:id, PUT /Groups/:id, PATCH /Groups/:id, DELETE /Groups/:id

User lists support filter=userName eq "dev@example.com", startIndex, and count. Group lists support the same paging fields. User PATCH accepts active:false for immediate offboarding; Group PATCH accepts add/remove/replace member operations. A missing group member can be created when the member carries a verified email in display or value.

Configure the bearer, admin group, and team mappings through the deployment-owner admin lane:

Configure SCIM
curl -X PUT https://<your-anyray-gateway>/admin/scim/settings \
-H "Authorization: Bearer $ANYRAY_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"bearerToken": "synthetic-scim-bearer-token-replace-me-0001",
"adminGroup": "Anyray Admins",
"groupTeamMap": { "Engineering": "platform" }
}'

GET /admin/scim/settings returns configured, adminGroup, groupTeamMap, and updatedAt. It never returns the bearer or bearer hash. A later PUT can omit bearerToken to retain the current credential.

SCIM errors always use application/scim+json and this envelope:

SCIM error
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"],
"detail": "invalid bearer token",
"status": "401"
}

Errors

Most inference errors use the OpenAI envelope ({ "error": { "message", "type", "code" } }). Client-key authentication uses the fixed machine envelope shown below.

StatusMeaning
400Malformed request: failed validation, bad JSON, or an unknown provider/model.
401Missing, malformed, unknown, expired, revoked, or SCIM-deactivated client identity. The body includes status: failure, a reason code, the fixed valid client key required message, and a safe hint.
402A governance ceiling has been reached: deployment entitlement/spend cap, a service key's monthly dollar budget, or a per-user monthly token cap / USD budget. The body carries error.code, one of service_key_budget_exceeded, user_token_cap_exceeded, or user_budget_exceeded. Terminal, not retryable: retrying cannot clear it, only an admin raising the limit or the window rolling.
429Rate limit exceeded. Honors Retry-After.
503A dependency the gateway needs is temporarily unreachable. Client-key verification returns status: failure with code: key_store_unavailable when the gateway cannot reach the store holding minted keys. This is an infrastructure fault, not a credential problem, so re-running anyray-connect will not help. Verification fails closed, so the request is never forwarded upstream. Retry shortly; if it persists, an operator should check GET /admin/health for the deployment's database legs.