# Errors

Look up the shape of an error response on each surface, what every error code means and what to do about it.

AuthFI reports a failure in the shape that the surface's standard defines. Find the surface you called, then the code.

## Response shapes

| Surface                                      | Shape                                                                  |
| -------------------------------------------- | ---------------------------------------------------------------------- |
| OAuth endpoints and the agent token endpoint | OAuth 2.0 (RFC 6749): `error`, and sometimes `error_description`       |
| The authorization endpoint                   | The same fields, with `state`, in the query string of the redirect URI |
| Other endpoints                              | A code in `error`, with the matching HTTP status                       |
| Zero-Code                                    | `403` with `error`, `message` and `ref`                                |
| MCP gateway                                  | A JSON-RPC error, or a tool result with `isError` set                  |

An OAuth error:

```json
{ "error": "invalid_client", "error_description": "client authentication failed" }
```

An error from another endpoint:

```json
{ "error": "invalid_body" }
```

A successful response from those endpoints puts its result under `data`:

```json
{ "data": { "decision": "allow" } }
```

## OAuth endpoints for applications

These codes come from the endpoints under `/oauth/`. Each endpoint is described in [OAuth and OpenID Connect endpoints](/reference/oauth#errors).

| `error`                             | Status | Meaning                                                      | What to do                                                                          |
| ----------------------------------- | ------ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| `invalid_request`                   | `400`  | A parameter is missing or malformed                          | Send every required parameter. Register the exact redirect URI, with path and port. |
| `invalid_client`                    | `401`  | The client is unknown, or its secret is wrong                | Check the client ID, the issuer and the client secret                               |
| `invalid_grant`                     | `400`  | The code or refresh token is wrong, expired or already used  | Start a new sign-in. Exchange each code once, within 5 minutes.                     |
| `unauthorized_client`               | `400`  | This application may not use this grant                      | Use an `m2m` application for `client_credentials`                                   |
| `unsupported_grant_type`            | `400`  | The grant type is not one the endpoint offers                | Use `authorization_code`, `refresh_token` or `client_credentials`                   |
| `invalid_scope`                     | `400`  | A refresh asked for more than was granted                    | Ask for a subset of the scopes first granted                                        |
| `invalid_target`                    | `400`  | `resource` is not something this caller may address          | Name the API's client ID, and grant the caller access to that API                   |
| `access_denied`                     | `403`  | The person is not assigned to the application                | Assign the application to the person or one of their groups                         |
| `login_required`                    | `401`  | The session is too old for `prompt=login` or `max_age`       | Send the person to sign in again                                                    |
| `interaction_required`              | `401`  | The person must complete another step                        | Send the person to the sign-in page                                                 |
| `unmet_authentication_requirements` | `400`  | The `acr_values` asked for could not be reached              | Enrol the person in MFA or a passkey, or ask for a lower value                      |
| `insufficient_user_authentication`  | `401`  | A stronger sign-in is needed before this token can be issued | Send the person to sign in with the `acr_values` required                           |
| `temporarily_unavailable`           | `503`  | The request could not be handled right now                   | Try again shortly                                                                   |
| `forbidden`                         | `403`  | The project's plan or account does not allow this            | Change the plan                                                                     |
| `rate_limited`                      | `429`  | Too many attempts                                            | Wait, then send fewer requests                                                      |

Presenting a used refresh token revokes every refresh token from the same sign-in and returns `invalid_grant`. See [Refresh tokens](/applications/sessions#keep-a-person-signed-in).

Guides that use these codes: [Add sign-in to an app](/quickstart/sign-in), [Sessions](/applications/sessions), [Application types](/applications/types) and [Service to service](/applications/machine-to-machine).

## Agent token endpoint

These are the only values `error` takes at `/apis/regional/v1/ai/auth/token`, so your code can branch on them.

| `error`                   | Meaning                                                          | What to do                                                    |
| ------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- |
| `invalid_request`         | The body could not be read, or a field is missing                | Send the body form-encoded, with every required field         |
| `invalid_client`          | The agent is unknown or revoked, or its assertion did not verify | Check `kid`, `aud`, the clock, and that `jti` is new          |
| `invalid_grant`           | The grant is not valid for this client                           | Check `grant_type` and what was sent with it                  |
| `unauthorized_client`     | This client may not use this grant                               | Use a grant the agent may use                                 |
| `unsupported_grant_type`  | The grant type is missing or not one the endpoint offers         | Use `client_credentials`                                      |
| `invalid_scope`           | A requested scope is not allowed                                 | Remove the scope from the request                             |
| `invalid_target`          | `resource` names something this agent has not been granted       | Ask an administrator to grant the agent access to that target |
| `temporarily_unavailable` | The grant could not be checked right now                         | Try again shortly                                             |

An assertion is accepted once. Sending the same one again returns `invalid_client`. See [Give an AI agent an identity](/quickstart/agent) and [Accept agents at your own API](/agents/verify).

Four more codes apply while an agent polls for a person's answer with the CIBA grant. Each has the status `400`. See [Ask a named person](/agents/ciba).

| `error`                 | Meaning                                    | What to do                          |
| ----------------------- | ------------------------------------------ | ----------------------------------- |
| `authorization_pending` | The person has not answered                | Wait `interval` seconds, poll again |
| `slow_down`             | You are polling too fast                   | Wait longer between polls           |
| `access_denied`         | The person denied the request              | Stop                                |
| `expired_token`         | The request expired before anyone answered | Stop, or ask again                  |

## Other endpoints

Endpoints outside OAuth return a code in `error` with the matching HTTP status.

| Status | `error`        | Meaning                                                  | What to do                                 |
| ------ | -------------- | -------------------------------------------------------- | ------------------------------------------ |
| `400`  | `invalid_body` | The request body is missing a field or is not valid JSON | Send valid JSON with every required field  |
| `401`  | `unauthorized` | No credential, or one that is expired or revoked         | Get a new credential and send it           |
| `403`  |                | The caller is known and is not allowed to do this        | Grant the caller access, or check the plan |
| `404`  |                | Nothing with that identifier exists in this project      | Check the identifier and the issuer        |
| `429`  |                | More than 300 requests a minute from one IP address      | Wait, then send fewer requests             |

Some endpoints use these statuses for a specific cause.

| Endpoint                      | Status | Cause                                         | Guide                                                               |
| ----------------------------- | ------ | --------------------------------------------- | ------------------------------------------------------------------- |
| `/saml/…`                     | `403`  | The plan does not include SAML single sign-on | [Connect a SAML application](/applications/saml)                    |
| `/saml/sso`                   | `403`  | The sign-in request failed validation         | [Connect a SAML application](/applications/saml)                    |
| `/scim/v2/…`                  | `403`  | The plan does not include SCIM                | [Provision people with SCIM](/connections/scim)                     |
| `/scim/v2/…`                  | `401`  | The SCIM token has expired                    | [Provision people with SCIM](/connections/scim)                     |
| `/v1/auth/resolve`            | `404`  | No connection claims that email domain        | [Sign in with another identity provider](/connections/sign-in-with) |
| Agent authorize and approvals | `401`  | The agent is unknown or revoked               | [Limit what an agent may do](/agents/limits)                        |

### Decisions

A decision is a successful response, not an error. The authorize endpoint returns `decision` and `reason` under `data`. See [Limit what an agent may do](/agents/limits).

```json
{ "data": { "decision": "deny", "reason": "constraint_violated: amount max" } }
```

| `decision`          | `reason`                         | Meaning                                | What to do                                                            |
| ------------------- | -------------------------------- | -------------------------------------- | --------------------------------------------------------------------- |
| `allow`             | `resource_grant`, `relationship` | Something granted it                   | Go ahead                                                              |
| `deny`              | `no_match`                       | Nothing granted it                     | Stop. To allow it, give the agent a role that carries the permission. |
| `deny`              | `policy_deny`                    | A rule denies it                       | Stop                                                                  |
| `deny`              | `constraint_violated: …`         | A limit on the capability was exceeded | Stop, or ask within the limit                                         |
| `approval_required` | `policy_step_up`                 | A rule sends it to a person            | Poll with `approval_id`                                               |

An approval that nobody answers reaches the status `expired`. Treat it as denied. See [Ask before a sensitive action](/agents/approvals).

## Zero-Code denials

A request denied at the node returns `403` with a reference that identifies the denial in the [audit record](/security/audit).

```http
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer realm="authfi", error="invalid_token"
X-Authfi-Ref: 7c1f0a2e
Content-Type: application/json

{ "error": "authfi_audience_mismatch", "message": "This credential was issued for a different service.", "ref": "7c1f0a2e" }
```

The checks run in this order on each request to a protected API. See [Protect an API](/zero-code/protect-api).

| Denied as             | Meaning                                                                                   | What to do                                                  |
| --------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `missing_token`       | The request has no `Bearer` token                                                         | Send an access token as `Authorization: Bearer`             |
| `invalid_token`       | The token is not signed by your project, has expired, or its agent or session was revoked | Get a new access token                                      |
| `not_an_access_token` | The token is an ID token                                                                  | Send the access token                                       |
| `audience_mismatch`   | The token was issued for another API                                                      | Ask for a token with `resource` set to this API's client ID |
| `policy_denied`       | The route's rule is not met                                                               | Give the caller what the rule requires, or change the rule  |

Other callers are answered differently. See [Protect a service with no code](/zero-code/overview) and [Enforcement in the kernel](/zero-code/enforcement).

| Caller                                           | Response                          | What to do                                          |
| ------------------------------------------------ | --------------------------------- | --------------------------------------------------- |
| A browser with no session                        | A redirect to the sign-in page    | Sign in                                             |
| A person not assigned to the application         | A `403` page that says so         | Assign the application to the person or a group     |
| A workload connecting where it was not granted   | The connection fails with `EPERM` | Add the destination and port to the workload's list |
| Any caller, when the node agent has no rules yet | `503` with `Retry-After`          | Retry after the time given                          |

In monitor mode nothing is denied. Each request that would have been denied is recorded with its reason.

## MCP gateway

The MCP gateway reports a failure at one of three levels. See [The MCP gateway](/agents/mcp-gateway).

**HTTP.** A request with no token, or one that is expired or revoked, returns `401` with a pointer to the [protected resource metadata](/reference/discovery#protected-resource-metadata).

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://acme.authfi.io/.well-known/oauth-protected-resource"
```

**JSON-RPC.** A request the gateway cannot process returns a JSON-RPC error.

| Code     | Meaning                                                      | What to do                                        |
| -------- | ------------------------------------------------------------ | ------------------------------------------------- |
| `-32700` | The body is not valid JSON                                   | Send valid JSON                                   |
| `-32601` | The method is not `initialize`, `tools/list` or `tools/call` | Use one of the three methods                      |
| `-32602` | The parameters are wrong                                     | Correct `params` and send the request again       |
| `-32000` | The tool server could not be reached                         | Check the server's URL in the console, then retry |

**Tool result.** A denied call is an ordinary tool result with `isError` set, so the model reads it and can change course.

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "isError": true,
    "content": [{ "type": "text", "text": "Denied by policy: agent:agt_7f3a9c21 is not granted payments.refund" }]
  }
}
```

| Result text begins  | Meaning                                                 | What to do                                                 |
| ------------------- | ------------------------------------------------------- | ---------------------------------------------------------- |
| `Denied by policy`  | The gateway denied the call. The text gives the reason. | Grant the tool, or change the arguments                    |
| `Approval required` | A rule sends the call to a person                       | After approval, repeat the call with `_authfi_approval_id` |

An approval is good for 15 minutes and for that call. A call to a tool with per-person credentials is denied when no person is named, or when the person has not connected their account.
