# Anlyon error codes

## Contents
- Authentication, scopes and plan
- Validation and conflicts
- Acting for end users
- End-user session tokens (React components)
- Invoking, approvals and previews
- Refusals inside a 200 response
- Resolving an unknown outcome

## Authentication, scopes and plan

| Code | Status | Meaning | Fix |
| --- | --- | --- | --- |
| `AUTHENTICATION_ERROR` | 401 | Key invalid, expired or revoked | New key; check `Authorization: Bearer <key>` |
| `AUTHORIZATION_ERROR` | 403 | Missing scope (named in the message) or role | New key with the scope, or the right role in the environment |
| `SELF_APPROVAL_NOT_ALLOWED` | 403 | A person tried to decide what their own agent requested | Another approver decides |
| `AUTHORIZATION_ERROR` "cannot decide an approval it requested" | 403 | A key or app tried to decide its own request | Decide in the console or with a different credential |
| `PLAN_FEATURE_REQUIRED` | 403 | The capability needs a higher plan | Admin upgrades, or skip the feature |
| `PAYMENT_REQUIRED` | 402 | No plan on the workspace | Admin: **Settings → Plan & billing** |
| `QUOTA_EXCEEDED` | 429 (some paths 402) | Monthly action allowance used up | The message says which allowance. Wait for the monthly reset; **Settings → Plan & billing** shows usage and options |
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests this minute | Back off; the SDK retries with the same idempotency key |
| `BUDGET_EXCEEDED` | 429 | The key hit its monthly cap on Anlyon operations | Wait for next UTC month, or an operator raises it (`budgets:write`) |
| `NOT_FOUND` | 404 | Not in this workspace and environment | Check the key's environment with `auth.identity()` |
| `EXPENSIVE_WORK_PAUSED` | 503 | New expensive work paused service-wide | Retry later with backoff |
| `ENVIRONMENT_HALTED` | 409 | Someone halted the environment (kill switch) | Nothing is sent until a person resumes it; tell the user |

## Validation and conflicts

| Code | Status | Meaning | Fix |
| --- | --- | --- | --- |
| `VALIDATION_ERROR` | 400 | Body or input failed validation; the message names the field | Fix the field. Action input is checked against `inputSchema` before anything is sent |
| `CONFLICT` | 409 | For example, an action name that exists or was archived | Choose a new name |
| `ACTION_VERSION_PIN_UNRESOLVED` | 409 | An agent version pins an action version that cannot be resolved | Fix the pin, or invoke without that `runId` |

## Acting for end users

| Code | Status | Meaning | Fix |
| --- | --- | --- | --- |
| `END_USER_REQUIRED` | 400 | Key requires an end user; none named | `anlyon.forUser(id)` / `client.for_user(id)` |
| `INVALID_END_USER` | 400 | Id is not 1 to 128 of `A-Z a-z 0-9 . _ : @ -` | Use your own user id, or a stable mapped id |
| `END_USER_HEADER_NOT_ALLOWED` | 400 | `Anlyon-End-User` sent with an OAuth token | Use an API key on your server |
| `END_USER_NOT_CONNECTED` | 409 | No account for the integration. Carries `integration`, `connectUrl` | Show `connectUrl`; retry after the user connects |
| `END_USER_RECONNECT_REQUIRED` | 409 | Account revoked or refresh failed | Same as above |
| `END_USER_MISMATCH` | 403 | Confirming a flow for a different user. Nothing stored | Confirm with the session's user; make a new link |
| `CONNECT_FLOW_NOT_FOUND` | 404 | Flow replayed, expired (10 minutes) or unknown | New connect link |
| `REDIRECT_URL_REQUIRED` | 400 | No `redirectUrl` and no default | Set the default redirect URL in **Integrations → Connect settings** |
| `REDIRECT_URL_NOT_ALLOWED` | 400 | `redirectUrl` not under an allowed redirect URL | Add it to the allowed list, or use the default |
| `OWN_OAUTH_APP_REQUIRED` | 400 | No shared app for this integration | Set the environment's own OAuth app (**Integrations → <integration> → Sign-in**) |
| `CONNECTIONS_UNAVAILABLE` | 501 | Signing in to this integration is not set up | Set the environment's own OAuth app |
| `CONNECTION_KEY_REJECTED` | 422 | The provider refused the user's key. Nothing stored | Ask the user for a valid key |
| `CONNECTION_KEY_CHECK_FAILED` | 502 | The key could not be checked with the provider | Retry later |

## End-user session tokens (React components)

| Code | Status | Meaning |
| --- | --- | --- |
| `END_USER_SESSION_REQUIRED` | 401 | No token sent |
| `END_USER_SESSION_EXPIRED` | 401 | Expired; the provider refetches and retries once |
| `END_USER_SESSION_INVALID` | 401 | Altered, minting key revoked, or the user erased |
| `END_USER_SESSION_SCOPE` | 403 | Minting key lacks `end-users:approvals` |
| `ORIGIN_NOT_ALLOWED` | 403 | Page origin is not an allowed redirect URL's origin |
| `END_USER_SESSION_NOT_ALLOWED` | 403 | Session token used on a route it cannot call |
| `END_USER_SESSION_API_KEY_REQUIRED` | 403 | Sessions are minted with an API key, not a console session or OAuth |

## Invoking, approvals and previews

| Code | Status | Meaning | Fix |
| --- | --- | --- | --- |
| `idempotency_key_reuse` | 409 | Same `Idempotency-Key` with a different body or credential | New key for a new operation |
| `idempotency_request_in_progress` | 409 | A resend arrived while the original still runs | Call again with the same key for the original's result |
| `OPERATION_KEY_REUSE` | 409 | Same `operationKey`, different request | New `operationKey` |
| `PREVIEW_MISMATCH` | 409 | The invoke does not match the reviewed preview | Invoke with the preview's exact `request`, or make a new preview |
| `PREVIEW_EXPIRED` | 409 | The preview's window passed | `actions.refreshPreview(id)` or a new preview |
| `PREVIEW_ALREADY_USED` | 409 | A preview runs once | New preview |
| `DEPENDENCY_UNRESOLVED` | 409 | A `dependsOn` effect is pending or unknown | Wait for it to settle |

## Refusals inside a 200 response

The invocation has `status: "failed"`, `grade: "refused"`, and `error: "Dispatch refused (<reason>): …"`.

| Reason | Meaning | Do |
| --- | --- | --- |
| `impact_limit_exceeded` | A limit has less room than the call needs. The call is refused whole | Tell the user; it frees at the period reset or when someone with `impact-limits:write` raises it |
| `impact_unbounded` | The declared amount reads an input the caller left out, and a limit applies | Pass that input; better, make it required in the schema |

A policy denial is `status: "denied"` with `decision.code: "POLICY_DENIED"`; an unanswered approval is `status: "expired"`.

## Resolving an unknown outcome

`unknown` means the request may have reached the provider. Anlyon never sends it again and keeps the limit capacity held.

1. Look for the result at the provider (the receipt's `providerReference`, the provider's dashboard).
2. `actions.reconcileEffect(effectId)` asks Anlyon to run the read-back now, when the action has one.
3. Otherwise an operator records the real outcome with a key holding `actions:resolve` (not the key that made the call): `actions.resolveInvocation(id, { outcome: 'succeeded' | 'failed', evidence })`, or in the console.
4. Once it is resolved as `failed`, a retry may go out, with `retryOf` and a new idempotency key.
