Skip to content

Authentication & keys

Every request to the gateway is authenticated with a gateway API key (gw_live_…).

SurfaceHeader
Provider proxy (/openai/…, /anthropic/…, …)The provider’s native auth header — Authorization: Bearer gw_live_… for OpenAI-style APIs, x-api-key: gw_live_… for Anthropic
Self-service API (/gw/…)Authorization: Bearer gw_live_…

The gateway replaces your key with the real provider credential server-side; your key never reaches the provider.

Each key carries a set of scopes:

ScopeAllows
inference:useCalling providers through the proxy
stats:readGET /gw/stats, GET /gw/usage, GET /gw/async, GET /gw/evidence, Stripe billing reads (GET /gw/stripe/config, GET /gw/stripe/events)
keys:manageGET /gw/ceiling, GET/POST /gw/keys, DELETE /gw/keys/{id}, sub-accounts (/gw/subaccounts), budgets (/gw/budgets), branding (/gw/branding), Stripe billing writes (PUT /gw/stripe/config, mappings, customers, event retry)

A request without the required scope gets 403.

Besides scopes, each key has entitlements — glob rules that control which models it may call. Each rule lives on one of three axes:

  • axis: "provider" (default) — (provider, model_pattern) over the inference provider and its native model id, as before.
  • axis: "vendor"(vendor, model_name) over the catalog slug author/model — the vendor glob matches the author-part (e.g. alibaba + qwen3-*): the rule follows the model through the catalog regardless of which provider hosts it.
  • axis: "kind"(kind, model_pattern) over the catalog modality (llm | image | video | 3d | embedding | rerank | audio) and the model name glob. Only matches catalog-known slugs, same as vendor. A deny rule on this axis only narrows access; an allow rule can only be granted to a child key if the issuing key’s own ceiling already carries that exact kind rule (kind-allow grants are operator-issued, not self-service).

Policy is default-deny and deny-wins across all axes: a model is allowed only if some allow rule matches and no deny rule matches. Calling a non-entitled model returns 403 before the request ever reaches the provider. Author and kind rules apply to models found in the catalog — a model the catalog does not know cannot be granted (or matched) by an author or kind rule.

Check what your key can do:

Terminal window
curl https://api.sociaro.com/gw/me -H "Authorization: Bearer gw_live_..."
# {"scopes":["inference:use"],"entitlements":[{"axis":"provider","provider":"openai","model_pattern":"gpt-4o*","effect":"allow"}]}

With a keys:manage key you can issue child keys scoped down for each service or teammate — see POST /gw/keys. Rules:

  • A child key’s scopes and entitlements must fit inside your organization’s ceiling (GET /gw/ceiling); anything beyond it is rejected with 403 and no key is created.
  • The plaintext key is returned exactly once — store it immediately.
  • Keys are immutable: to change permissions, revoke and issue a new one.
  • Each organization has a cap on active keys (default 1000; your operator can raise it per client). Hitting the cap rejects POST /gw/keys with 403; revoking a key frees a slot immediately.
  • Revocation (DELETE /gw/keys/{id}) is a soft delete — usage history is preserved. Revocation takes effect immediately on the gateway instance that handled the request; a revoked key may keep working for up to ~30 seconds on other gateway instances (auth cache TTL), or up to ~90 seconds if the database is briefly unavailable right as that cache entry expires (the cache serves the last-valid entry through a short grace window during an outage).

GET /gw/keys (scope keys:manage) lists your organization’s keys without their secrets. Each entry includes a last_used_at timestamp:

Terminal window
curl https://api.sociaro.com/gw/keys -H "Authorization: Bearer gw_live_..."
# [
# {"id":"<uuid>","key_prefix":"gw_live_ab12","status":"active",
# "scopes":["inference:use"],"sub_account_id":null,
# "created_at":"2026-05-01T10:00:00Z","last_used_at":"2026-06-13T08:41:12Z"}
# ]

How last_used_at is maintained:

  • Every authenticated request stamps the key’s last_used_at asynchronously and best-effort — the write runs in a background goroutine and never blocks or fails your request.
  • It is throttled to roughly one write per key per 60 seconds, so a key hammered with traffic is not stamped on every call. Treat the value as approximate to within that window, not a precise last-request clock.
  • It is null until the key is used for the first time.
  • Under a multi-instance deployment each instance throttles independently, so the stamp may lag by up to the throttle window.

last_used_at is surfaced only by GET /gw/keys. GET /gw/me describes the current key and returns only {scopes, entitlements} — it does not include last_used_at. Use it to spot dormant keys you can safely revoke.

  • Treat gw_live_… like any production secret: environment variables or a secret manager, never source control.
  • Issue one key per service/environment so revocation is surgical.
  • Use the least scopes possible — a server that only does inference needs nothing beyond inference:use.