Agent manual
Everything an AI agent needs to manage providers, applications, users and usage through the API, in one page.
Manage App AI Gateway on a person's behalf with the CLI or an existing MCP
connection. This manual is also served as plain markdown at
https://docs.appaigateway.com/agents.md.
Use the documentation index to find task-specific guides and
OpenAPI for exact HTTP schemas. Prefer the connected server's
capabilities and the installed CLI's --help when versions differ.
Choose the connection before changing anything
Hosted defaults:
| Purpose | Address |
|---|---|
| Console, sign-in and browser handoffs | https://console.appaigateway.com |
| App requests and management API | https://api.appaigateway.com |
| MCP | https://console.appaigateway.com/mcp |
For self-hosting, substitute that deployment's console and API origins; they
may be the same. Do not deploy infrastructure unless the person asked for
self-hosting. GATEWAY_URL below means the selected API origin, with no /v1
suffix. Use the URLs returned by handoff operations instead of constructing them.
With MCP, start with get_account and read agw://guide; see "MCP" below.
For the CLI, install @maxceem/agw on Node.js 22.19 or later, then:
agw deployment status --json --no-input
agw account status --json --no-inputThe CLI defaults to hosted only when no connection is saved. Inspect its reported URL, deployment and account; do not assume a previous task left the right connection active. A configured credential is not proof it is valid.
- Existing account: use the saved connection if it matches. Otherwise run
agw account login --no-open --json --no-input. To sign in to hosted again, useagw deployment connect --cloud --no-open --json --no-input. Without a supplied management key, this returns a pending browser login just likeaccount login; have the person approve it before continuing. - Existing self-hosted gateway: use
agw deployment connect --url URL --no-open --json --no-input. - New hosted account, when requested: on a fresh hosted connection, the
first provider, provider-gateway or app creation initializes an unclaimed
account. Do not use this path to recover a failed login to an existing one.
Check account status for its expiry, start
agw account claim --no-open --json --no-input, and give the person the URL to keep the account.
Help, status, lists, validation and dry runs never create accounts. Claiming requires the person to create their sign-in in their own browser; do not open or approve the claim yourself. Claiming preserves the agent's access.
CLI task workflow
- Read
agw provider list --jsonandagw app list --json. Reuse the IDs and slugs returned; inspect a target withagw app show <app-id> --json. - Add a missing provider with
agw provider add --type openai --browser --no-open --json --no-input. Give the person the returned URL and wait withagw operation wait <id> --json. Never request the secret in chat. - Read the command's
--helpand make the requested change. For iOS, supply the person's Team ID and Bundle ID. Set--attest-environments productionfor a production-only app: CLI flag-based creation otherwise enables both production and development. For server keys, use--key-outputwith an unused private path, or retain the CLI's managed private output file. - For a new app file, use
agw app validate --file app.json --json. For an existing app, useagw app update <app-id> --file app.json --dry-run --jsonso validation is performed as an update of that app. The file contains onlyname,configandstatusfromresult.app; exclude metadata such asid,revisionand timestamps. Keep every existing configuration field. - Apply the requested update, then reread the app. File updates replace the whole document. The CLI reads a revision when the update command starts; this protects against a competing write during that command, not edits made since an earlier file export. Refresh and merge an old file first.
- Run
agw app check <app-id> --jsonandagw app snippet <app-id> --json. Resolve any placeholders named inresult.notes. The check reads configuration only: no provider key, physical device, sign-in or entitlement is exercised. Do not send inference unless the person requested it. - Report the app ID, provider slugs, API base URL, key file location, checks and remaining work. Report pending handoffs as pending, never completed.
For every unattended command, use --json --no-input; for browser handoffs,
add --no-open. --json returns {schemaVersion:1,ok:true,context,result} or
{schemaVersion:1,ok:false,error}. Inspect both ok and the result: a successful
pending operation still needs the browser step and polling. Exit codes are
0 success, 2 invalid input, 3 remote failure, 4 state/authentication recovery
and 5 wait timeout. A timeout does not cancel the operation. Follow
error.nextAction, preserving retry state after a lost response.
Use agw usage show --month YYYY-MM --json for retained account totals,
including deleted apps. Configuration files transfer neither credentials nor
usage history between deployments.
Signing in
agw account login --no-open --jsonopens a login and returnsstate: "pending"withurl,userCodeandid. Give the person the URL and the pairing code. They open the URL in their own browser, check that the page shows the same code, and approve. Then runagw operation wait <id> --json; the CLI stores its new management key and prints only non-secret fields. Without--no-openthe CLI also opens the page in the local browser when one is available (not over SSH, on Linux with no display, or without a terminal), and without--jsonit waits for the approval itself and prints the pairing code to stderr.- For your own deployment,
agw deployment connect --url URLruns the same login against that URL. - A declined login fails with
operation_denied; an expired one withoperation_expired. Start a new login; do not retry the old id. - In CI, or when the person gives you an existing management key, use the
AGW_MANAGEMENT_KEYenvironment variable. When it is set it is the credential for every command, ahead of any saved login, and the environment key is not saved to disk;agw account loginrefuses withenvironment_credential. Alternatively pipe the key once withagw account login --key-stdin(oragw deployment connect --url URL --key-stdin) to save it. Never pass a key as an argument. agw account logout --jsonrevokes the saved key and removes it locally. Its result is{loggedOut: true, revoked: true|false};revoked: falsemeans the key was already refused or the deployment could not be reached.- Every key a CLI login creates appears on the console's Access page,
tagged CLI and labelled with the machine it was issued to, for example
CLI on mac-studio. The person can revoke it there at any time.
MCP
If you are connected to the gateway as an MCP server, at the console's address
followed by /mcp (https://console.appaigateway.com/mcp when hosted), you
can work with the same account through its tools instead of the CLI. A client
connects with OAuth by itself: the person approves it on the console's consent
page, choosing the account and whether you may read or manage, or continues
without an account, which creates an unclaimed account your connection manages.
An unclaimed account expires unless a person claims it: when get_account
says so, call claim_account and give the person its URL. A management key as
a bearer token works too. Use the tools:
get_account first, then list_providers, list_apps, get_app,
validate_app, check_app, usage and event tools, and the change tools.
add_app and add_app_key take two calls: the first reserves and creates
nothing, the second, with the same arguments and the answer's handle,
creates it once, and repeating it is safe. An answer that names an operation
calls it id; that is what get_operation takes. A key is revealed on a page a
person opens, never in a tool result: give them reveal_url. If a read contains
[redacted] configuration, do not send that marker back in an update or drop
those fields from a full replacement; have the person preserve them through
the console or another authorized client. A provider key
or gateway token is never a tool argument: add_provider and the rotate tools
answer a URL where the person enters it, and get_operation reports when they
have finished. A refusal is a tool result marked as an error with the
gateway's code and the next step to take. The server's agw://guide resource
has its full rules, and the MCP server guide at /automation/mcp lists every
tool.
The API reference below is useful for integrations that do not use the CLI.
What the gateway is
App AI Gateway is a proxy between a person's applications and AI providers
(OpenAI, Anthropic, Gemini, xAI and others). The person adds provider keys
once; they are shared by all their apps. They create applications; each
app has a permanent ID, an authentication policy, a proxy policy saying
which providers, paths and models it may use, optional custom endpoints,
and limits on its users. Client apps call
{GATEWAY_URL}/v1/apps/{app}/proxy/{provider_slug}/{provider_path} with the
provider's own request format. You manage all of this through the admin API.
Authentication
- Use the explicitly selected gateway URL; the hosted API is
https://api.appaigateway.com.GET /v1/healthzchecks basic availability. - Direct integrations send a management credential as
Authorization: Bearerusing their protected credential store. The CLI usesAGW_MANAGEMENT_KEYwhen it is set, and otherwise the key its active connection saved. - All admin routes are under
/v1/admin/. Request and response bodies are JSON withContent-Type: application/json. - A management key has the current role of its owning identity. It does not
expire. It cannot perform human-only actions such as claiming an account,
changing human ownership, or creating, listing and revoking other management
keys, which are console-only and answer
403 session_requiredto a key. A key can revoke only itself, withDELETE /v1/cli/credential; that is whatagw account logoutsends.
Rules
- Never print, log or echo any secret: management keys, provider keys, application API keys. When a call returns a key, store it in a private file where the person asked, then report its location.
- Never send
idwhen creating an application. The gateway assigns the ID from the name and returns it asapp.id. IDs cannot be changed. - Validate an application body before writing it: a new application with
POST /v1/admin/app-drafts/validate, an existing one withPOST /v1/admin/apps/{app}/validate. PUT /v1/admin/apps/{app}replaces the whole application. AlwaysGETfirst, modify the returnedapp.name,app.configandapp.status, and send all three back together with theapp.revisionfrom that read. A missing revision returns 400; a stale revision returns 409. Reread and merge.- Read before you write. List providers and apps before changing anything, so you use slugs and IDs that exist.
- App and provider changes take up to one minute to apply to live traffic. Blocking an authenticated user refuses new token exchanges immediately; existing gateway tokens stop working within ten seconds. Header-based users must be blocked in the backend.
- Prefer reversible actions. Disable an app or provider rather than deleting it unless deletion was asked for. Deleting a provider also deletes its custom prices; deleting an app removes its users and keys.
- Do not test the setup by sending provider requests unless asked, because each one costs the person money at the provider.
Errors
Every refusal is JSON:
{ "error": { "code": "slug_taken", "message": "…", "data": { } } }| Status | Codes | Meaning |
|---|---|---|
| 400 | invalid_request | The body is malformed or violates a rule; message says which. A create body that carries id gets this |
| 401 | auth_required | Missing or wrong management key |
| 403 | forbidden | The owning identity's role does not permit this operation |
| 403 | grant_insufficient | The key is read-only and this call would change something; ask the person for a key with the manage grant |
| 404 | not_found, app_not_found | No such app, provider, gateway or key |
| 409 | slug_taken | A provider already has this slug; supply another |
| 409 | gateway_in_use | A gateway still has providers routed through it |
| 409 | provider_gateway_managed | This provider's key lives in a gateway; rotate the gateway token instead |
| 400 | provider_not_supported_by_gateway | That provider type cannot be routed through that gateway |
| 400 | provider_key_invalid | The provider refused the key during a test |
| 400 | app_revision_required | Include the revision from the app read |
| 409 | app_revision_conflict, conflict | Reread the resource and merge the requested changes before retrying |
| 502 | provider_unavailable | The gateway could not decrypt a stored key |
Workflow for setting up an app
GET /v1/admin/providers. Note which provider types exist and theirslugvalues.- If a provider is missing, use a CLI or MCP browser handoff so the person can enter the key privately.
- Compose the application body from the reference below.
POST /v1/admin/app-drafts/validatewith the body. Fix every error.- Create through the retry-safe operation, using
kind: "app.add"and that body aspayload. Readresult.app.idand storeresult.api_key.keyprivately for a server app. A plainPOST /v1/admin/appsinstead returnsappandapi_keyat the top level, but cannot safely be retried after a lost response. - Report the app ID, the base URL
{GATEWAY_URL}/v1/apps/{app.id}/proxy/{provider_slug}/{provider_path}, and where the key went. Do not include the key.
Providers
A provider row is one key for one provider type, or a reference to a gateway
that holds the key. Its slug is the URL segment clients use. The first
provider of each type defaults to the type name as its slug (openai,
anthropic, gemini, xai, perplexity, deepseek, groq, mistral,
together, fireworks, cerebras, moonshot, huggingface, baseten,
bytedance, openrouter). A second provider of the same type must be given
a slug, or the create answers 409 slug_taken.
| Call | Body | Notes |
|---|---|---|
GET /v1/admin/providers | Returns providers[] with id, type, slug, name, secretHint, providerGatewayId, baseUrl, pricing, route, capability, revision, status. capability lists the apiStyles and endpointStyles the instance serves on its route and the modelPrefix the route adds | |
POST /v1/admin/providers/test | { type, secret } or { type, providerGatewayId }, optional baseUrl | Dry run. validated: true means the provider accepted the key. validated: false with a reason means nothing was proven. A refused key answers 400 provider_key_invalid |
POST /v1/admin/providers | { type, name, secret } or { type, name, providerGatewayId }; optional slug, baseUrl, pricing, gatewayRoute | Exactly one of secret and providerGatewayId. baseUrl only with secret |
PUT /v1/admin/providers/{id} | revision plus any of name, secret, baseUrl, pricing, gatewayRoute, status | revision is the value from the provider read this edit is based on. secret rotates the key in place. A new non-null baseUrl requires secret in the same call. pricing: null clears prices. status: "disabled" pauses without freeing the slug |
DELETE /v1/admin/providers/{id} | Permanent. Frees the slug, deletes custom prices |
pricing is per model, in USD per million tokens:
{ "my-model": { "input": 0.5, "output": 1.5, "cached_input": 0.05 } }input and output are required. Any other per-token rate the catalog uses
is optional under the same name: cached_input, cache_write,
cache_write_1h, image_input, audio_input, video_input,
image_output, and the long_* rates, which also need
long_context_threshold. A custom price replaces the catalog's rates for that
model and inherits none of them: a rate left out bills as on a catalog entry
without it, so a cache rate falls back to input.
A model that is in neither the built-in catalog (GET /v1/admin/prices)
nor the provider's pricing cannot be used by any app: requests answer
400 pricing_not_configured, and an app configuration that names it in
allowed_models, model_rewrites targets or endpoints is rejected on
validation. OpenRouter reports its own cost and needs no price, and so does
xAI on Chat Completions, Responses and image generation, directly or through
Cloudflare; its Messages API, v1/stt and the Vercel route still need one.
The catalog prices OpenAI's gpt-image-* and Gemini's image models per token,
with separate image rates, and BytePlus Seedream 5.0 flash, 5.0 lite, 4.5 and
4.0 per generated image. Other image models billed per image or per
megapixel, such as Together AI's and Seedream 5.0 pro, are not supported, and
per-token pricing cannot express them.
Custom baseUrl points an openai-type provider at an OpenAI-compatible
server. It requires https, a public hostname, the default port and no query.
The client path is appended literally: use https://my-vllm.example.com/
with v1/chat/completions, or Azure's https://RESOURCE.openai.azure.com/openai/
with that same path. Do not duplicate v1/. See Custom base URLs.
Gateways
A gateway row is a connection to a Cloudflare AI Gateway or Vercel AI Gateway whose token is stored once and shared by providers routed through it.
| Call | Body |
|---|---|
GET /v1/admin/provider-gateways | |
POST /v1/admin/provider-gateways/test | { type: "cf_aig", accountId, gatewayId, token } or { type: "vercel", token } |
POST /v1/admin/provider-gateways | Same plus name |
PATCH /v1/admin/provider-gateways/{id} | { name, revision } |
POST /v1/admin/provider-gateways/{id}/rotate | { token, revision } |
DELETE /v1/admin/provider-gateways/{id} | Refused with 409 gateway_in_use while any provider references it |
Provider and gateway updates use the revision returned by their list route.
A missing or invalid revision returns 400. If another edit wins first, the
write returns 409 conflict; read the row again and merge before retrying.
Cloudflare routes openai, anthropic, xai, gemini, perplexity.
Vercel routes those plus deepseek and moonshot, and accepts only the
paths v1/responses, v1/chat/completions and v1/messages.
Applications
| Call | Body | Notes |
|---|---|---|
GET /v1/admin/apps | apps[] with each app's month-to-date usage | |
GET /v1/admin/apps/{app} | { app: { id, name, config, status, revision, created_at, updated_at, warnings } } | |
POST /v1/admin/app-drafts/validate | { name, config, status? } | { valid: true } or a 400 naming the problem. For an app that does not exist yet |
POST /v1/admin/apps/{app}/validate | { name, config, status? } | { valid: true, app_id } or a 400 naming the problem. Judged as an update of that app; 404 if it does not exist |
POST /v1/admin/apps | { name, config, status? } | Returns the app plus api_key (server apps only, once) |
PUT /v1/admin/apps/{app} | { name, config, status?, revision } | Full replace; revision must be the one the last read returned |
DELETE /v1/admin/apps/{app}?confirm={app} | Query repeats the app ID. Removes users, keys and diagnostics; retains usage history in account totals | |
GET /v1/admin/apps/{app}/keys | Server apps only | |
POST /v1/admin/apps/{app}/keys | { name } | Returns key once |
POST /v1/admin/apps/{app}/keys/{key}/revoke |
status is active or disabled. A disabled app refuses every request
with 403 app_disabled and keeps everything.
An app's warnings describe behaviour that will stop on the stated date:
each has a code, a message, and usually until and docs_url. Tell the
person about every warning and help them act before its date. Warnings are
read-only: leave warnings, id, created_at and updated_at out of the
body you write back, or the write is refused, and keep the revision from the
read, which an update requires.
Application configuration reference
config is one JSON object with four top-level keys. Unknown keys are
rejected.
{
"authentication": { … },
"routing": { … },
"limits": { … },
"endpoints": { … }
}authentication
Discriminated on type.
iOS application (apple_app_attest):
| Key | Type | Meaning |
|---|---|---|
type | "apple_app_attest" | The app proves itself with Apple App Attest |
app_attest.team_id | string | Apple Team ID |
app_attest.bundle_id | string | Bundle ID |
app_attest.environments | ["production"], ["development"] or both; optional | Which App Attest environments are accepted. Omitted means production only |
app_attest.verify_on | "every_request" or "token_exchange"; optional | When the install's App Attest assertion is verified. every_request, the default when omitted: every request carries X-AppAttest-Proof, which needs the Swift package 1.4 or later. token_exchange: only at the hourly token exchange, and requests carry the token alone. Apps created before this setting was added were set to token_exchange. Switch an app to every_request only after the person confirms every build in use is on package 1.4 or later; older builds are refused from then on |
end_user | object, required | { "source": "issuer", "issuer": {…} } for signed-in users, or { "source": "app_install" } to count each install as a user |
Server application (api_key):
| Key | Type | Meaning |
|---|---|---|
type | "api_key" | The backend proves itself with an API key |
end_user | object, required | { "source": "none" } for no user identity. { "source": "header", "header": "x-end-user-id" } when the backend names the user in a header. { "source": "issuer", "issuer": {…} } for signed-in users, in which case the key is exchanged with the user's ID token for a gateway token |
The issuer object (signed-in users):
| Key | Type | Meaning |
|---|---|---|
jwks_url | URL | Where the identity provider publishes its signing keys |
issuer | string or string[] | Accepted iss values. Required |
audience | string or string[] | Accepted aud values. Required |
user_id_claim | string | Claim used as the user ID. Use "sub" |
required_claims | array | Entitlement requirements, each { "path": "...", "contains": "x" } or { "path": "...", "equals": value }. contains may be a list of alternatives. Empty array for none |
max_token_lifetime_seconds | integer | Reject tokens whose exp - iat exceeds this. Use 86400 |
token_header | string, optional | Rarely needed |
provider | "firebase", "supabase", "auth0", "clerk", "custom", optional | Lets the console show the preset. Set it to match what you configured |
entitlement | "revenuecat" or "custom", optional | Same, for the subscription check |
Preset values for common identity providers:
| Provider | jwks_url | issuer | audience |
|---|---|---|---|
| Firebase | https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com | https://securetoken.google.com/<project-id> | <project-id> |
| Supabase | https://<ref>.supabase.co/auth/v1/.well-known/jwks.json | https://<ref>.supabase.co/auth/v1 | authenticated |
| Auth0 | https://<domain>/.well-known/jwks.json | https://<domain>/ (trailing slash required) | the API identifier |
| Clerk | https://<frontend-api-host>/.well-known/jwks.json | https://<frontend-api-host> | the aud the JWT template sets |
| Sign in with Apple | https://appleid.apple.com/auth/keys | https://appleid.apple.com | the bundle ID |
A RevenueCat entitlement check is
{ "path": "revenueCatEntitlements", "contains": "<entitlement id>" } with
"entitlement": "revenuecat".
routing
| Key | Type | Meaning |
|---|---|---|
providers.mode | "all" or "selected" | all allows every provider the account has, now and later. selected allows only those in providers.selected |
providers.selected | object keyed by provider slug | Required when mode is selected. Each value is a policy object below. Every slug must be an existing provider on create |
model_rewrites | object | Client model name to real model name. Required key; use {} for none. Targets must be priced |
Policy object for one provider:
| Key | Type | Meaning |
|---|---|---|
allowed_paths | array | Empty allows inference APIs, and each provider's own image, embedding, speech and legacy completion endpoints; the text ones honour max_output_tokens, the rest are never capped. A non-empty array allows only its entries, as strings like "v1/responses" or objects { "path", "fixed_model"?, "clamp"? }. clamp is one of responses, chat_completions, gemini_native, anthropic, none |
allowed_models | string[] | Empty allows every priced model |
max_output_tokens | positive integer, optional | Requests above it are refused; requests without an output limit get it injected |
limits
Optional. Omit for no limits. Every field is null (unlimited) or a number.
Non-null per_user limits require an app that identifies users; an all-null
block is accepted without user identity. Request limits
must be positive; a spend budget may be 0 to stop all spend.
On an iOS app, per_user request limits count per install (one copy of the
app on one device), and so does the budget unless
per_user.spending.scope is "user", which needs end_user.source
issuer. scope defaults to "install" on iOS apps and "user" on server
apps, where "install" is refused. The gateway writes the default in.
"limits": {
"per_user": { "requests": { "per_minute": 10, "per_day": 300 }, "spending": { "monthly_usd": null } },
"per_app": { "requests": { "per_minute": null, "per_day": null }, "spending": { "monthly_usd": 100 } }
}endpoints
Optional. Keyed by slug (^[a-z0-9-]{1,64}$). Each endpoint:
| Key | Type | Meaning |
|---|---|---|
api_style | "responses", "chat_completions", "anthropic_messages", "gemini_native", "audio_transcription", "image_generation", "embeddings" or "gemini_embedding" | Request format the client sends |
kind | "text", "image", "speech", "transcription" or "embedding", optional | What the endpoint does. Defaults to the format's only kind; gemini_native carries text (the default), image and speech |
provider | provider slug | A provider whose type and route serve the format: OpenAI and xAI for responses, the OpenAI-compatible types and OpenRouter for chat_completions, Anthropic for anthropic_messages, Gemini for both Gemini formats, OpenAI, xAI and Groq for audio_transcription, OpenAI, xAI and ByteDance for image_generation, OpenAI for embeddings |
model | string | Must be of the endpoint's kind and priced. A model only custom pricing covers counts as text. On a provider that reports its own cost, an unpriced model is accepted |
params | object, optional | Deep-merged over the client body, server wins. Every format but audio_transcription |
max_output_tokens | positive integer, optional | Text endpoints only |
fallback | array of { provider, model }, optional | Tried in order when the primary fails before streaming starts. Same format and kind |
listModelPrices gives every catalog model's kind: text, image,
speech, transcription, embedding or realtime. Pick an endpoint's
models by it; a realtime model serves no endpoint.
Complete examples
A server app for a backend that names its users, restricted to one OpenAI model:
{
"name": "Search service",
"config": {
"authentication": {
"type": "api_key",
"end_user": { "source": "header", "header": "x-end-user-id" }
},
"routing": {
"providers": {
"mode": "selected",
"selected": {
"openai": { "allowed_paths": ["v1/responses"], "allowed_models": ["gpt-5.6-sol"], "max_output_tokens": 4096 }
}
},
"model_rewrites": {}
},
"limits": {
"per_user": { "requests": { "per_minute": 30, "per_day": 1000 }, "spending": { "monthly_usd": 10 } },
"per_app": { "requests": { "per_minute": 300, "per_day": 10000 }, "spending": { "monthly_usd": 100 } }
}
}
}An iOS app with Firebase sign-in, a RevenueCat entitlement, and a named endpoint:
{
"name": "Calorie Tracker",
"config": {
"authentication": {
"type": "apple_app_attest",
"app_attest": { "team_id": "ABCDE12345", "bundle_id": "com.example.calorietracker" },
"end_user": {
"source": "issuer",
"issuer": {
"jwks_url": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com",
"issuer": "https://securetoken.google.com/calorie-1a2b3",
"audience": "calorie-1a2b3",
"user_id_claim": "sub",
"required_claims": [{ "path": "revenueCatEntitlements", "contains": "pro" }],
"max_token_lifetime_seconds": 86400,
"provider": "firebase",
"entitlement": "revenuecat"
}
}
},
"routing": { "providers": { "mode": "all" }, "model_rewrites": {} },
"limits": {
"per_user": { "requests": { "per_minute": 20, "per_day": 500 }, "spending": { "monthly_usd": 5 } },
"per_app": { "requests": { "per_minute": null, "per_day": null }, "spending": { "monthly_usd": 500 } }
},
"endpoints": {
"chat": {
"api_style": "responses",
"provider": "openai",
"model": "gpt-5.6-luna",
"params": { "reasoning": { "effort": "low" } },
"fallback": [{ "provider": "xai", "model": "grok-4.5" }]
}
}
}
}Users
| Call | Query or body | Notes |
|---|---|---|
GET /v1/admin/apps/{app}/users | month=YYYY-MM, `status=active | blocked, query=, limit=, offset=` |
GET /v1/admin/apps/{app}/users/{user} | ||
POST /v1/admin/apps/{app}/users/{user}/block | New token exchanges are refused immediately; existing gateway tokens stop working within ten seconds. Users named by a backend header cannot be blocked | |
POST /v1/admin/apps/{app}/users/{user}/unblock |
Usage and events
| Call | Query | Returns |
|---|---|---|
GET /v1/admin/apps/{app}/usage | month=YYYY-MM | Totals: requests, tokens, cost |
GET /v1/admin/apps/{app}/usage/timeseries | from=, to= (ISO dates) | Daily buckets by provider |
GET /v1/admin/apps/{app}/usage/breakdown | by=, from=, to=, limit= | Grouped totals. by is one of model, model_author, provider, provider_slug, provider_gateway, credential_source, user, install, status, cost_source, route, endpoint, app_version |
GET /v1/admin/apps/{app}/events | limit=, status=, user=, install=, model=, before_id= | Individual provider attempts, newest first, each with install_id on iOS apps. Page with next_before_id |
GET /v1/admin/apps/{app}/auth-events/summary | days= | Token exchange success rate, failures by cause, claim delay percentiles |
GET /v1/admin/apps/{app}/auth-events | limit=, outcome=, before_id= | Individual token exchanges and registrations |
GET /v1/admin/apps/{app}/rejection-events | limit=, reason=, scope=, user=, from=, to=, before_id= | Sampled requests stopped before a provider call; page with next_before_id |
Usage event status values: ok, provider_error. Refusal sample reason
values: blocked_app_rate, blocked_app_budget, blocked_user,
blocked_billing, blocked_replay (a replayed App Attest proof). scope is
install, user, app or account. Samples are limited to one per identity per minute, so
sample counts are not exact request totals. Event
cost_source values: computed, reported, unresolved.
What clients send
You do not normally call these, but you may need to explain them or write client code.
- Proxy:
{METHOD} /v1/apps/{app}/proxy/{provider_slug}/{provider_path}with the provider's own body. Credential:Authorization: Bearer <gateway token>plusX-App-Versionfor iOS and signed-in-user apps, orAuthorization: Bearer agw_…for a server app without user identity. When an iOS app'sverify_onisevery_request, every request also carriesX-AppAttest-Proof, an App Attest assertion over that request; without it the gateway answers401 assertion_required. Withtoken_exchangethe token alone is enough. - Custom endpoint:
POST /v1/apps/{app}/endpoints/{slug}. - Token exchange for a server app with signed-in users:
POST /v1/apps/{app}/auth/tokenwith{ "api_key", "issuer_token" }, returning{ "access_token", "expires_in": 3600 }. - iOS apps use the AppAIGateway Swift package 1.4 or later, which handles App
Attest, the token exchange and, when the token exchange answers
verify_on: "every_request", the per-request proof. Send requests withwithAuthorizedRequest(1.4.1 or later): it builds the request, hands it to the app's own send closure, and on a401discards the token and sends a rebuilt request once. Pass the request body to it, never set it afterwards.authorizedRequestbuilds the request alone, for an app that handles the retry itself. See the human documentation under Integrate your app.
For Auth0's API-identifier preset, issuer_token must be a JWT access token
for that API. Firebase uses its ID token. In every case the configured issuer
and audience must match; see User authentication.
Contacting human support
Where the deployment offers human support, you can write to it on the
person's behalf: agw support send with the CLI, contact_support over MCP,
or POST /v1/admin/support with a management key. Support replies to the
person's own email address, never to you.
- Only when the person asked you to contact support. Show them the exact
message first, and send it only once they agree. Without a terminal,
agw support sendrefuses to send unless you pass--yes, which says you have done this. - Choose the type:
bugfor something broken,supportfor a question or a request for help,feedbackfor an idea or a missing feature. - Write what happened, what they expected and what was tried. Put ids and
codes in context so support can act without asking back:
--context app_id=my-app --context request_id=… --context error_code=…. - Never include a key, token or password. A message or context value that
looks like one is refused with
invalid_request, and nothing is sent. - Tell the person the answer's
reference, such asSUP-7F3K2Q. The same message sent again within ten minutes sends nothing new: the answer hasduplicate: trueand the first reference, so do not resend to make sure. - If the gateway refuses because the account cannot use human support, tell the person what the message says instead of retrying.
agw support send --type bug --message "Streaming responses from my-app stop after about 30 seconds since Monday." \
--context app_id=my-app --context error_code=provider_timeout --yes --json --no-inputReporting back
When you finish a task, report: what you created or changed, by ID and slug; the client base URL for any app; where each secret was stored; and anything you could not do, such as a provider whose key was not available. Do not include secret values.
Retrying creation safely
A plain POST to create an app, application key, provider or provider gateway
is not safe to repeat: a response lost in transit leaves you not knowing
whether it landed. Send the creation as an operation instead:
POST /v1/cli/operations with { kind, payload, token }, where kind is
app.add, app.key.add, provider.add or provider-gateway.add, payload
is the body the plain endpoint takes (plus app for app.key.add), and
token is a random URL-safe string of 32–256 characters you persist before
sending. Repeat the same kind, payload and token to recover the original
result; a different payload under the same token is 409. A new application
key is returned in result.api_key.key for 15 minutes after creation; past
that the result still names the resource and the key's id, and no second key
is minted. Records are kept for 90 days.