AppAIGatewayDocs
Integrate your app

Errors and limits

Common client errors, their response format, and how to handle limits and failures.

Every refusal from the gateway is JSON with one error object:

{
  "error": {
    "code": "app_rate_limited",
    "message": "Per-user rate limit exceeded",
    "data": { "scope": "user" }
  }
}

code is stable and is what your client should switch on. message is for logs. data is present only when the code alone is not actionable. Every 429 carries a Retry-After header in seconds.

A provider's own error is not in this format: it is passed through as the provider sent it, with the provider's status code. Provider errors may also contain error.code, so that field alone does not prove where an error originated. Handle known gateway codes, retain the HTTP status and provider request ID when present, and inspect Usage and Auth & Errors to diagnose the source.

Authentication

CodeStatusMeaningWhat to do
auth_required401 or 403No usable credential, an expired gateway token, a revoked key, or a blocked userRefresh an expired gateway token once. On iOS, withAuthorizedRequest in the Swift package 1.4.1 or later does this for you on a 401 and returns a 403 as it is; if you send a request built with authorizedRequest, call discardAccessToken() and build it again, once. If it persists, check the app credential and the user status in the console
assertion_required401A request with an iOS app's gateway token arrived without the X-AppAttest-Proof header, on an app whose Verify App Attest on is Every request. Only that setting answers this codeOn the Swift package 1.4 or later, a request built by the package sees this only when the app was switched to Every request after the client's token was issued. withAuthorizedRequest, in 1.4.1 or later, handles it: it discards the token and sends a rebuilt, signed request once. If you send a request built with authorizedRequest, call discardAccessToken() and build the request again, once. A request assembled by hand from gatewayAccessToken() must be built with the package instead. On an earlier package, update to 1.4 or later, or set Verify App Attest on to Token exchange while older builds are still in use
attest_failed403An App Attest proof was refused. At the token exchange: the attestation or assertion did not verify. On a request: the X-AppAttest-Proof header is malformed, signed by a key that is not registered or whose environment is no longer accepted, signed over a different request, or its assertion counter was already used. The message says whichwithAuthorizedRequest in the Swift package returns it to you rather than retrying it. Build a new request and send it once. If it persists, call discardAccessToken() and build the request again: the fresh exchange re-registers the key when the gateway no longer knows it. That does not help when the key's environment is no longer accepted or the request was changed after signing
issuer_token_rejected403The sign-in token did not verifyRefresh the ID token, retry once, then treat as signed out
issuer_claims_missing403The token verified but the required entitlement claim is not there yetWait and retry. Do not sign the user in again. Sync the purchase if you can
issuer_verification_unavailable503The identity provider's keys could not be fetchedRetry with backoff
auth_method_not_supported404The app is not configured for this call, such as a token exchange on an app without user identityFix the app's configuration
invalid_request400A malformed body, a missing X-App-Version, or a missing end-user headerFix the request
rate_limited429Too many calls to challenge, register or token for this app from one network address. data.scope names the endpoint: app_auth_challenge, app_auth_register or app_auth_tokenWait Retry-After seconds. A real app almost never reaches this, so look for a retry loop of your own, or a lot of devices sharing one address

rate_limited is the gateway protecting these three endpoints itself, and is counted per app and per address. It is not one of the limits you set on your app below, and nothing in the console changes it.

Limits

CodeStatusMeaningWhat to do
app_rate_limited429A per-minute or per-day request limit set on this app refused the call. data.scope is install, user or appWait Retry-After seconds. An install or user scope means slow this caller down; an app scope means the whole app is busy
app_budget_exhausted429A monthly spending budget set on this app is spent. Same data.scopeWait for the next month, or raise the budget in the console
billing_request_quota_exceeded429The monthly request allowance of the account that owns the app is spent. data carries limit, used and resetAtNothing on the device fixes it. Show a friendly message and retry after resetAt
billing_unavailable503The gateway could not check the account, so it refused rather than guessRetry after Retry-After. Not an account problem
billing_payment_required402Account access needs attention in the consoleAsk the person managing the gateway account to resolve it; retrying on the device will not help
payload_too_large413The request body is over 20 MBSend less

app_* codes come from limits set on the app in the console. billing_* codes come from the account and are unrelated to those limits.

Account access

These refusals concern the gateway account, not your app's end-user sign-in:

CodeStatusWhat to do
unclaimed_access_expired403The person who started the account must claim it through the CLI or MCP browser handoff. Do not ask the app's end user to sign in again.
account_expired403The account's recovery deadline has passed. Stop automatic retries and ask the person managing the gateway account to investigate.
email_unverified403The gateway account's owner has not verified their email address yet. They open the link in the verification email, or send a new one from the console; requests are served from then on. Do not ask the app's end user to sign in again.

Policy

CodeStatusMeaningWhat to do
path_not_allowed403The app's proxy policy does not allow this provider or pathUse an allowed path, or allow it in the console
model_not_allowed403The model is not in the app's allowlistUse an allowed model
max_output_tokens_exceeded403The request asks for more output than the app allowsLower the output limit in the request
api_style_not_supported403This path does not exist on the route the provider uses, such as Gemini's native API through VercelUse a supported path or a direct provider
pricing_not_configured400The model has no price in the catalog or the provider's custom pricingAdd a price in the console, or use a catalogued model
endpoint_not_found404No custom endpoint has this slugCheck the slug
app_not_found404No app has this IDCheck the app ID
app_disabled403The app is switched off in its settingsNothing on the device fixes it

Providers

CodeStatusMeaningWhat to do
provider_not_configured502No provider has this slugAdd one in the console
provider_disabled502The provider with this slug is pausedEnable it in the console
provider_unavailable502The provider's key could not be readCheck the gateway's health
provider_error502 or 504The provider could not be reached, or sent no response headers before the timeout (120 seconds by default)Retry with backoff
service_unavailable503The infrastructure the gateway runs on failed or was overloaded, or a sign-in or /me call took longer than 10 seconds. Nothing about the request is known to be wrongRetry after Retry-After with backoff
internal_error500Something failed inside the gatewayRetry with backoff

A sensible client policy

  • Use bounded backoff for transient 429 and 5xx errors, respecting Retry-After. A budget or account refusal needs the action in its row above. Avoid automatically replaying inference after a partial response or a lost connection: the provider may already have processed and charged for it.
  • On app_rate_limited with scope install or user, back that caller off for Retry-After seconds. An iOS app counts request limits per install. With scope app, show a "busy, try again shortly" state rather than hammering.
  • On issuer_claims_missing, keep the user signed in and poll gently.
  • Send X-App-Version on every request, so the console can tell you which release a problem belongs to.
  • Log code and message, never the request body or the token.

The Swift package's GatewayError exposes code, retryAfter, limitScope and isRetryable for exactly this. See iOS with the Swift package.

On this page