AppAIGatewayDocs
Automation and agents

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:

PurposeAddress
Console, sign-in and browser handoffshttps://console.appaigateway.com
App requests and management APIhttps://api.appaigateway.com
MCPhttps://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-input

The 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, use agw deployment connect --cloud --no-open --json --no-input. Without a supplied management key, this returns a pending browser login just like account 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

  1. Read agw provider list --json and agw app list --json. Reuse the IDs and slugs returned; inspect a target with agw app show <app-id> --json.
  2. Add a missing provider with agw provider add --type openai --browser --no-open --json --no-input. Give the person the returned URL and wait with agw operation wait <id> --json. Never request the secret in chat.
  3. Read the command's --help and make the requested change. For iOS, supply the person's Team ID and Bundle ID. Set --attest-environments production for a production-only app: CLI flag-based creation otherwise enables both production and development. For server keys, use --key-output with an unused private path, or retain the CLI's managed private output file.
  4. For a new app file, use agw app validate --file app.json --json. For an existing app, use agw app update <app-id> --file app.json --dry-run --json so validation is performed as an update of that app. The file contains only name, config and status from result.app; exclude metadata such as id, revision and timestamps. Keep every existing configuration field.
  5. 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.
  6. Run agw app check <app-id> --json and agw app snippet <app-id> --json. Resolve any placeholders named in result.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.
  7. 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 --json opens a login and returns state: "pending" with url, userCode and id. 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 run agw operation wait <id> --json; the CLI stores its new management key and prints only non-secret fields. Without --no-open the 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 --json it waits for the approval itself and prints the pairing code to stderr.
  • For your own deployment, agw deployment connect --url URL runs the same login against that URL.
  • A declined login fails with operation_denied; an expired one with operation_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_KEY environment 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 login refuses with environment_credential. Alternatively pipe the key once with agw account login --key-stdin (or agw deployment connect --url URL --key-stdin) to save it. Never pass a key as an argument.
  • agw account logout --json revokes the saved key and removes it locally. Its result is {loggedOut: true, revoked: true|false}; revoked: false means 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/healthz checks basic availability.
  • Direct integrations send a management credential as Authorization: Bearer using their protected credential store. The CLI uses AGW_MANAGEMENT_KEY when it is set, and otherwise the key its active connection saved.
  • All admin routes are under /v1/admin/. Request and response bodies are JSON with Content-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_required to a key. A key can revoke only itself, with DELETE /v1/cli/credential; that is what agw account logout sends.

Rules

  1. 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.
  2. Never send id when creating an application. The gateway assigns the ID from the name and returns it as app.id. IDs cannot be changed.
  3. Validate an application body before writing it: a new application with POST /v1/admin/app-drafts/validate, an existing one with POST /v1/admin/apps/{app}/validate.
  4. PUT /v1/admin/apps/{app} replaces the whole application. Always GET first, modify the returned app.name, app.config and app.status, and send all three back together with the app.revision from that read. A missing revision returns 400; a stale revision returns 409. Reread and merge.
  5. Read before you write. List providers and apps before changing anything, so you use slugs and IDs that exist.
  6. 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.
  7. 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.
  8. 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": { } } }
StatusCodesMeaning
400invalid_requestThe body is malformed or violates a rule; message says which. A create body that carries id gets this
401auth_requiredMissing or wrong management key
403forbiddenThe owning identity's role does not permit this operation
403grant_insufficientThe key is read-only and this call would change something; ask the person for a key with the manage grant
404not_found, app_not_foundNo such app, provider, gateway or key
409slug_takenA provider already has this slug; supply another
409gateway_in_useA gateway still has providers routed through it
409provider_gateway_managedThis provider's key lives in a gateway; rotate the gateway token instead
400provider_not_supported_by_gatewayThat provider type cannot be routed through that gateway
400provider_key_invalidThe provider refused the key during a test
400app_revision_requiredInclude the revision from the app read
409app_revision_conflict, conflictReread the resource and merge the requested changes before retrying
502provider_unavailableThe gateway could not decrypt a stored key

Workflow for setting up an app

  1. GET /v1/admin/providers. Note which provider types exist and their slug values.
  2. If a provider is missing, use a CLI or MCP browser handoff so the person can enter the key privately.
  3. Compose the application body from the reference below.
  4. POST /v1/admin/app-drafts/validate with the body. Fix every error.
  5. Create through the retry-safe operation, using kind: "app.add" and that body as payload. Read result.app.id and store result.api_key.key privately for a server app. A plain POST /v1/admin/apps instead returns app and api_key at the top level, but cannot safely be retried after a lost response.
  6. 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.

CallBodyNotes
GET /v1/admin/providersReturns 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 baseUrlDry 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, gatewayRouteExactly one of secret and providerGatewayId. baseUrl only with secret
PUT /v1/admin/providers/{id}revision plus any of name, secret, baseUrl, pricing, gatewayRoute, statusrevision 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.

CallBody
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-gatewaysSame 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

CallBodyNotes
GET /v1/admin/appsapps[] 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}/keysServer 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):

KeyTypeMeaning
type"apple_app_attest"The app proves itself with Apple App Attest
app_attest.team_idstringApple Team ID
app_attest.bundle_idstringBundle ID
app_attest.environments["production"], ["development"] or both; optionalWhich App Attest environments are accepted. Omitted means production only
app_attest.verify_on"every_request" or "token_exchange"; optionalWhen 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_userobject, required{ "source": "issuer", "issuer": {…} } for signed-in users, or { "source": "app_install" } to count each install as a user

Server application (api_key):

KeyTypeMeaning
type"api_key"The backend proves itself with an API key
end_userobject, 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):

KeyTypeMeaning
jwks_urlURLWhere the identity provider publishes its signing keys
issuerstring or string[]Accepted iss values. Required
audiencestring or string[]Accepted aud values. Required
user_id_claimstringClaim used as the user ID. Use "sub"
required_claimsarrayEntitlement requirements, each { "path": "...", "contains": "x" } or { "path": "...", "equals": value }. contains may be a list of alternatives. Empty array for none
max_token_lifetime_secondsintegerReject tokens whose exp - iat exceeds this. Use 86400
token_headerstring, optionalRarely needed
provider"firebase", "supabase", "auth0", "clerk", "custom", optionalLets the console show the preset. Set it to match what you configured
entitlement"revenuecat" or "custom", optionalSame, for the subscription check

Preset values for common identity providers:

Providerjwks_urlissueraudience
Firebasehttps://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.comhttps://securetoken.google.com/<project-id><project-id>
Supabasehttps://<ref>.supabase.co/auth/v1/.well-known/jwks.jsonhttps://<ref>.supabase.co/auth/v1authenticated
Auth0https://<domain>/.well-known/jwks.jsonhttps://<domain>/ (trailing slash required)the API identifier
Clerkhttps://<frontend-api-host>/.well-known/jwks.jsonhttps://<frontend-api-host>the aud the JWT template sets
Sign in with Applehttps://appleid.apple.com/auth/keyshttps://appleid.apple.comthe bundle ID

A RevenueCat entitlement check is { "path": "revenueCatEntitlements", "contains": "<entitlement id>" } with "entitlement": "revenuecat".

routing

KeyTypeMeaning
providers.mode"all" or "selected"all allows every provider the account has, now and later. selected allows only those in providers.selected
providers.selectedobject keyed by provider slugRequired when mode is selected. Each value is a policy object below. Every slug must be an existing provider on create
model_rewritesobjectClient model name to real model name. Required key; use {} for none. Targets must be priced

Policy object for one provider:

KeyTypeMeaning
allowed_pathsarrayEmpty 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_modelsstring[]Empty allows every priced model
max_output_tokenspositive integer, optionalRequests 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:

KeyTypeMeaning
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", optionalWhat the endpoint does. Defaults to the format's only kind; gemini_native carries text (the default), image and speech
providerprovider slugA 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
modelstringMust 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
paramsobject, optionalDeep-merged over the client body, server wins. Every format but audio_transcription
max_output_tokenspositive integer, optionalText endpoints only
fallbackarray of { provider, model }, optionalTried 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

CallQuery or bodyNotes
GET /v1/admin/apps/{app}/usersmonth=YYYY-MM, `status=activeblocked, query=, limit=, offset=`
GET /v1/admin/apps/{app}/users/{user}
POST /v1/admin/apps/{app}/users/{user}/blockNew 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

CallQueryReturns
GET /v1/admin/apps/{app}/usagemonth=YYYY-MMTotals: requests, tokens, cost
GET /v1/admin/apps/{app}/usage/timeseriesfrom=, to= (ISO dates)Daily buckets by provider
GET /v1/admin/apps/{app}/usage/breakdownby=, 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}/eventslimit=, 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/summarydays=Token exchange success rate, failures by cause, claim delay percentiles
GET /v1/admin/apps/{app}/auth-eventslimit=, outcome=, before_id=Individual token exchanges and registrations
GET /v1/admin/apps/{app}/rejection-eventslimit=, 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> plus X-App-Version for iOS and signed-in-user apps, or Authorization: Bearer agw_… for a server app without user identity. When an iOS app's verify_on is every_request, every request also carries X-AppAttest-Proof, an App Attest assertion over that request; without it the gateway answers 401 assertion_required. With token_exchange the 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/token with { "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 with withAuthorizedRequest (1.4.1 or later): it builds the request, hands it to the app's own send closure, and on a 401 discards the token and sends a rebuilt request once. Pass the request body to it, never set it afterwards. authorizedRequest builds 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.

  1. 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 send refuses to send unless you pass --yes, which says you have done this.
  2. Choose the type: bug for something broken, support for a question or a request for help, feedback for an idea or a missing feature.
  3. 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=….
  4. 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.
  5. Tell the person the answer's reference, such as SUP-7F3K2Q. The same message sent again within ten minutes sends nothing new: the answer has duplicate: true and the first reference, so do not resend to make sure.
  6. 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-input

Reporting 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.

On this page