# When a call fails

| Status / error | What it means | What to do |
| --- | --- | --- |
| `401` `invalid_token`, `expired` | the access token aged out | refresh at `https://api.apiosk.com/functions/v1/agent-gateway/v1/oauth/token`, then retry once. Silent — not worth narrating |
| `401` `unauthorized`, other | the token is revoked or unknown | the user disconnected, or the refresh chain broke. Reconnect from `https://apiosk.com/skill/references/connecting.md` |
| `403` `approval_per_request_limit`, `approval_daily_limit`, or `limit_exceeded` in a task's errors | the plan is over a per-request or daily ceiling | **not retryable.** Say which limit was hit and stop. You cannot raise it; the user changes it in Apiosk > Connections |
| `403` `approval_balance_insufficient` | the balance will not cover the ceiling | tell the user the balance is short and stop. Only they can top up |
| `403` `approval_refused` | this connection may not spend in that workspace | tell the user; it is decided in Apiosk |
| `403` `public_api_required` | an organisation key reached the task routes | organisation keys use the public API: `POST /v2/ask` |
| `410` `moved_to_gateway_v2` | a removed v1 route on `https://api.apiosk.com/functions/v1/agent-gateway` | send the request to `https://gateway.apiosk.com` instead; the body names the `replacement` |
| status `needs_input` | a required value was absent | ask the user for it. Never invent one |
| status `state_conflict` | the task moved on | adopt the returned `state` and reassess its actions. Do not replay an old paid action |
| `429` `busy` or `rate_limited` | too many requests | back off; retry with the **same** `request_id` and idempotency key |
| `503` `approval_unconfirmed` or `execution_pending`, a `5xx`, or a timeout on `/v2/approve` or `/v2/execute` | unknown whether it landed | read `GET /v2/tasks/{id}` first. Retry only the identical request with the **same** idempotency key. That returns the original outcome rather than buying it twice |

## The rule that costs money to get wrong

A request that times out may have succeeded. The idempotency key is what lets
you ask "did that land?" without paying to find out — the same key returns the
same outcome, a new key buys a second call. A retry loop that mints a fresh key
on each attempt is a loop that spends the balance. Reading the task is free, so
read it first.

## Things that look like failures and are not

- **`unsupported`** — no published source does that job. Say so. Do not fall
  back to guessing or to your own knowledge and present it as a result.
- **`partial`** — part of the question was answered. Answer that part and say
  what is missing; do not imply complete coverage.
- **`pending_reconciliation` in billing** — the financial outcome is not known
  yet. It is never a reason to pay again.
- **A higher ceiling than the user expected** — that is the price. There is no
  second number to quote and no discount to look for.
- **A refresh that returns a different refresh token** — that is rotation working.
  Store the new one and discard the old.

## What never to do about an error

Do not put a token, an invitation or an eight-digit code into an error report,
into generated code, or into the conversation. If you need to show the user what
failed, show the status and the message — `https://gateway.apiosk.com` returns a sentence, not a
stack trace, and the sentence is the part they can act on.
