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

# Structured Errors

> Category, code, and required action in every error — so agents recover programmatically.

Every error from WalletSuite includes a `category`, `code`, and `message`. Errors in the `flow` category also include a `requiredAction` — a specific instruction for what the caller should do next.

The contract is identical across the REST API, MCP tools, and the SDK — only the wrapper around it differs. Agents and integrators can recover programmatically instead of guessing.

## Error Format

```json theme={null}
{
  "category": "validation",
  "code": "INVALID_ADDRESS",
  "message": "Address does not match expected format for ethereum"
}
```

For `flow` errors, a `requiredAction` is always included:

```json theme={null}
{
  "category": "flow",
  "code": "TRANSFER_NOT_PREPARED",
  "message": "No prepared transfer matches this request.",
  "requiredAction": "Call prepare_transfer to build the transfer, then submit the returned intent for signing."
}
```

## Error Categories

| Category        | Where it appears | Meaning                                                                         | What the caller should do                        |
| --------------- | ---------------- | ------------------------------------------------------------------------------- | ------------------------------------------------ |
| `validation`    | SDK + MCP        | Bad input — address format, missing field, invalid amount                       | Fix the input and retry                          |
| `upstream`      | SDK + MCP        | Backend API or RPC failure, possibly transient                                  | Retry with exponential backoff                   |
| `flow`          | MCP only         | Prerequisite missing in the MCP signing flow — e.g. a transfer not yet prepared | Execute the `requiredAction` first, then retry   |
| `auth`          | SDK + MCP        | API key invalid, missing, or unauthorized                                       | Stop — tell the user to configure credentials    |
| `limit`         | SDK + MCP        | Rate or quota exceeded                                                          | Back off, retry after the rate limit window      |
| `not_available` | SDK + MCP        | Feature not enabled for this API key                                            | Inform the user the feature is not on their plan |

## Common Error Codes

Every error carries a `code`. The **Source** column tells you which layer raised it — and which surfaces can see it:

* **API** — returned by the WalletSuite REST API. Visible to SDK and MCP callers alike.
* **MCP** — raised by MCP tool orchestration (signing layer, policy engine, schema pre-validation, tool dispatch). **Not returned by the REST API** — SDK callers do not encounter these.

### Validation Errors

| Code                     | Source | When It Happens                                                                                                     |
| ------------------------ | ------ | ------------------------------------------------------------------------------------------------------------------- |
| `INVALID_ADDRESS`        | MCP    | Address fails the format check for the specified chain (e.g., not 0x-prefixed for EVM) — raised before the API call |
| `INVALID_AMOUNT`         | MCP    | Human-readable amount is not a positive decimal string                                                              |
| `INVALID_AMOUNT_WEI`     | MCP    | Wei amount is not a positive integer string                                                                         |
| `MISSING_TOKEN_CONTRACT` | MCP    | Token transfer attempted with a symbol but no contract address — use `resolve_asset` first                          |
| `INVALID_PARAMS`         | MCP    | Schema validation failed on tool input                                                                              |
| `BAD_REQUEST`            | API    | Backend returned 400 — check the request parameters                                                                 |
| `NOT_FOUND`              | API    | Backend returned 404 — the resource does not exist                                                                  |

### Flow Errors — MCP only

Flow errors signal a missing prerequisite in the MCP signing flow — for example, a transfer that must be prepared before it can be signed. They are raised by MCP orchestration and **never returned by the REST API**, so SDK callers do not encounter them. (Policy denials are separate and reach every surface — see [Policy Denials](#policy-denials).)

| Code                    | When It Happens                                                      | Required Action                                                                            |
| ----------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `TRANSFER_NOT_PREPARED` | A sign or send request references a transfer that was never prepared | Call `prepare_transfer` to build the transfer, then submit the returned intent for signing |
| `INTENT_EXPIRED`        | The prepared intent is no longer valid for signing                   | Call `prepare_transfer` again to build a fresh intent, then submit it                      |

Policy denials are covered in [Policy Denials](#policy-denials) below.

### Auth Errors

| Code           | Source | When It Happens                                                               |
| -------------- | ------ | ----------------------------------------------------------------------------- |
| `UNAUTHORIZED` | API    | API key is missing or invalid (backend returned 401)                          |
| `FORBIDDEN`    | API    | API key is valid but not authorized for this operation (backend returned 403) |

### Upstream Errors

| Code                       | Source | When It Happens                                                     |
| -------------------------- | ------ | ------------------------------------------------------------------- |
| `SERVER_ERROR`             | API    | Backend returned 500, 502, or 503                                   |
| `TIMEOUT`                  | API    | Request exceeded the timeout (default 60s)                          |
| `NETWORK`                  | API    | DNS or connection failure                                           |
| `TX_COMPILER_*`            | API    | Transaction compilation failed at the backend                       |
| `SIMULATION_FAILED`        | API    | Transaction simulation at the backend failed                        |
| `INVALID_BACKEND_RESPONSE` | MCP    | Backend response did not match the expected schema (MCP-side guard) |

### Limit and Feature Errors

| Code                  | Source | When It Happens                                                                                                                  |
| --------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `RATE_LIMIT_EXCEEDED` | API    | Backend returned 429 — too many requests                                                                                         |
| `PLAN_RESTRICTED`     | API    | Backend returned 403 — feature not enabled on your API key                                                                       |
| `FEATURE_GATED`       | MCP    | MCP tool classified as not-available for your plan (surfaces `PLAN_RESTRICTED` to the agent as a `not_available` category error) |

### Policy Denials

The policy engine evaluates every signing intent on every surface — MCP, SDK, and REST. When your organization's policy denies an intent, WalletSuite records it in state `denied` with a decision reason and a `policyDecisionId`, and no signature is produced. On MCP this arrives as a `flow` error whose `message` carries the decision reason and whose `requiredAction` tells the agent how to proceed; SDK and REST callers receive the same decision reason and `policyDecisionId`.

A policy denial is deterministic. The same request denies again until the request or the policy changes, so do not blindly retry. Quote the `policyDecisionId` when raising a support ticket.

| Reason                   | When It Happens                                                     | What the caller should do                                                 |
| ------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `chain_not_entitled`     | The intent's chain is outside the chains you're entitled to         | Sign on an entitled chain, or have the chain added to your plan           |
| `no_matching_rule`       | The org allowlist is armed and no active rule admits this operation | Adjust the request to satisfy an active rule, or have an admin add a rule |
| `unclassified_operation` | The org allowlist is armed and the operation could not be typed     | Use a supported operation type                                            |

See [Policy Gates](/core-concepts/policy-gates) for the rule model and [Policy Decisions](/core-concepts/policy-decisions) for the decision record on every intent.

## Consuming errors

The same structured payload reaches the caller through two different wrappers. Pick the sub-section that matches your surface.

### From an AI agent (MCP)

Every MCP tool response surfaces the structured error verbatim. The `category` field tells the agent what class of action to take; `requiredAction` (when present) gives a concrete next step.

A well-behaved agent should:

1. Check `category`
2. If `flow` — execute the `requiredAction`, then retry the original request
3. If `validation` — fix the input based on the `message`, then retry
4. If `upstream` — retry with exponential backoff (3 attempts, then surface to user)
5. If `auth` — stop and ask the user to fix their credentials
6. If `limit` — wait and retry
7. If `not_available` — inform the user

### From the SDK

SDK calls throw an `ApiError` wrapper. The structured payload lives in `err.bodySnippet` as a JSON string — parse it to reach `category`, `code`, and `message`.

SDK callers see the API-source categories: `validation` (from the backend), `auth`, `upstream`, `limit`, and `not_available` — plus **policy denials**, which reach every surface with the decision reason and `policyDecisionId`. The `flow` category (MCP orchestration prerequisites) does not appear in SDK responses.

```typescript theme={null}
import { WalletSuiteSDK, ApiError } from "@walletsuite/wallet-sdk";

try {
  const balance = await sdk.api.getNativeBalance(address, "ethereum");
} catch (err) {
  if (err instanceof ApiError && err.bodySnippet) {
    try {
      const payload = JSON.parse(err.bodySnippet);
      switch (payload.category) {
        case "validation":    /* fix input, retry */         break;
        case "upstream":      /* retry with backoff */       break;
        case "auth":          /* check credentials */        break;
        case "limit":         /* wait and retry */           break;
        case "not_available": /* inform the user */          break;
      }
    } catch {
      // non-JSON body — transport-level failure (timeout, DNS, etc.)
    }
  }
}
```

For SDK-specific wrapper fields (`url`, `method`, `status`, `attempt`) and debug-logging behavior, see [SDK Error Handling](/sdk/error-handling).

## Design Principle

Errors are designed so agents can recover without human intervention whenever possible. A `flow` error with a `requiredAction` gives the agent a concrete next step — it does not need to guess or ask the user for help.

Internal error details (`internalMessage`) are logged server-side but never included in the response sent to the LLM. The agent sees only what it needs to act on.
