> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tallyforagents.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> How Tally returns errors and what each type means.

Every error response from the Tally API uses a consistent envelope. The HTTP status code tells you the rough category; the `type` and `code` in the body tell you the specifics.

## Envelope

```json theme={null}
{
  "error": {
    "type": "validation_failed",
    "message": "Amount exceeds per-tx max.",
    "code": "amount_too_large",
    "details": { "max_per_tx_usdc": "10", "requested": "15" }
  }
}
```

Every error has `type` and `message`. `code` is set when there's a more specific failure mode within a `type` (most payment policy violations carry one). `details` carries structured context when relevant — typically the failed Zod issue list for validation errors.

## Error types

| `type`              | HTTP | Meaning                                                                                                 |
| ------------------- | ---- | ------------------------------------------------------------------------------------------------------- |
| `unauthenticated`   | 401  | Missing, malformed, or invalid API key.                                                                 |
| `forbidden`         | 403  | The request authenticated, but a policy check rejected it. Inspect `code`.                              |
| `not_found`         | 404  | The resource doesn't exist in this (account, mode).                                                     |
| `validation_failed` | 400  | The request body or query failed validation. `details` carries the Zod issues.                          |
| `conflict`          | 409  | The request collides with existing state. Not used by `v1` endpoints today but reserved for future use. |
| `rate_limited`      | 429  | The account is over its per-second ceiling. See [Rate limits](/api/rate-limits).                        |
| `internal`          | 500  | A dependency failed (Privy, the chain). Retryable; see specific `code` for the cause.                   |

The SDK maps each type to a typed exception class — see [SDK errors](/sdk/errors). REST callers branch on `type` and `code` directly.

## Codes on `POST /v1/payments`

The payments endpoint is the only `v1` route with structured `code` values today. Branch on `error.type` first, `error.code` second.

| `code`                  | `type`              | Meaning                                                                                                                                                                                                                                                                                                                |
| ----------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agent_not_found`       | `not_found`         | No agent with that `agent_id` in this (account, mode).                                                                                                                                                                                                                                                                 |
| `wallet_not_found`      | `not_found`         | The wallet address isn't owned by this account, or is in the wrong mode.                                                                                                                                                                                                                                               |
| `permission_not_found`  | `not_found`         | The agent has no active grant on that wallet.                                                                                                                                                                                                                                                                          |
| `amount_invalid`        | `validation_failed` | `amount_usdc` is malformed (non-decimal, too many decimal places, etc.).                                                                                                                                                                                                                                               |
| `address_invalid`       | `validation_failed` | `wallet` or `to` is not a valid EVM address.                                                                                                                                                                                                                                                                           |
| `amount_too_large`      | `forbidden`         | Exceeds the grant's per-transaction max.                                                                                                                                                                                                                                                                               |
| `daily_cap_exceeded`    | `forbidden`         | Would push the rolling 24-hour spend over the grant's daily cap.                                                                                                                                                                                                                                                       |
| `insufficient_balance`  | `forbidden`         | The wallet doesn't have enough USDC to cover the requested amount. Surfaced when Privy's enclave (or the chain) rejects the call as `ERC20: transfer amount exceeds balance`. Recovery is to fund the wallet — distinct from the other `forbidden` codes, which are policy violations the user can't unilaterally fix. |
| `recipient_not_allowed` | `forbidden`         | `to` is not in the grant's recipient allowlist.                                                                                                                                                                                                                                                                        |
| `contract_not_allowed`  | `forbidden`         | The destination contract is not on the grant's contract allowlist (USDC is the default).                                                                                                                                                                                                                               |
| `permission_expired`    | `forbidden`         | The grant's `expires_at` has passed.                                                                                                                                                                                                                                                                                   |
| `privy_call_failed`     | `internal`          | Privy's API failed during signing for a reason Tally couldn't classify (the catch path before falling back here pattern-matches the user-fixable cases like `insufficient_balance`). Retryable.                                                                                                                        |

The `forbidden`-typed codes are policy violations, not auth failures. Surface them in your UI as "this permission doesn't permit that," not "your credentials are bad."

## Recovery patterns

| Type                | Should you retry?                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `unauthenticated`   | No. Fix the key or rotate.                                                                                          |
| `forbidden`         | No, not the same way. Either fix the request shape (different `to`, smaller `amount_usdc`) or wait for a new grant. |
| `not_found`         | No. Either the id is wrong or the resource doesn't exist yet.                                                       |
| `validation_failed` | No. Fix the input.                                                                                                  |
| `conflict`          | Maybe. Depends on the specific endpoint; today no `v1` route emits this.                                            |
| `rate_limited`      | Yes, after `Retry-After`. Use exponential backoff.                                                                  |
| `internal`          | Yes, with an idempotency key. The cause is downstream.                                                              |

Always read `error.message` for human context before deciding — the message often points at the specific field or constraint that failed.

## Sample handlers

### Branching on `type` and `code`

```ts theme={null}
async function pay(input: PaymentInput) {
  const res = await fetch(`${baseUrl}/v1/payments`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(input),
  });

  if (res.ok) return res.json();

  const { error } = await res.json();

  switch (error.type) {
    case "forbidden":
      if (
        error.code === "amount_too_large" ||
        error.code === "daily_cap_exceeded"
      ) {
        // Policy violation — prompt to top up the permission.
        return notifyToTopUpPermission(error);
      }
      if (error.code === "insufficient_balance") {
        // Wallet ran out of USDC. Distinct recovery: fund the wallet
        // (not the permission). Don't retry without action.
        return notifyToFundWallet(error);
      }
      throw new PolicyError(error);
    case "rate_limited":
      await sleep(Number(res.headers.get("Retry-After") ?? 1) * 1000);
      return pay(input); // retry with the same idempotency key
    case "internal":
      // Tally or Privy hiccup; retry idempotently
      return pay(input);
    default:
      throw new ApiError(error);
  }
}
```

### Logging unknown errors

When you encounter a `type` your handler doesn't branch on, log the entire envelope and surface a generic failure to the user. Don't drop the body — the `details` field often has exactly the info you need to diagnose later.
