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

# Errors

> Branching on ApiError.code to drive recovery UI.

Every SDK hook surfaces errors as `ApiError`. Each `ApiError` carries
three fields:

* `code` — the API's stable machine-readable identifier (e.g.
  `refresh_invalid`). May be `undefined` if the response body was
  missing or malformed.
* `status` — the HTTP status code (e.g. `401`, `404`).
* `message` — human-readable, may reword.

Branch on `code` for recovery logic; show `message` in toasts /
banners; use `status` for catch-all 4xx-vs-5xx checks.

## Import

```ts theme={null}
import { ApiError } from "@calm-xyz/react";
```

## Usage

```tsx app/page.tsx theme={null}
"use client";
import { ApiError, useCalm, useSession } from "@calm-xyz/react";

function SessionStatus() {
  const { address } = useCalm();
  const { error } = useSession({ address });

  if (error?.code === "refresh_invalid") {
    // The refresh cookie is gone or stale — sign the user out at the
    // wallet/identity layer and remount the provider.
    return <button onClick={signOut}>Sign back in</button>;
  }

  if (error) {
    return <p role="alert">{error.message}</p>;
  }

  return null;
}
```

## Error codes

The codes below are stable across SDK minor versions. Catch-all on
`status` for codes not listed (rare; the API tries to emit a stable
code on every 4xx).

### Session

| code                      | status    | when                                                                                                                                                                      |
| ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `refresh_invalid`         | 401       | The browser's refresh cookie is missing, expired, or wallet-mismatched. Sign out at your wallet/identity layer and remount the provider.                                  |
| `invalid_token`           | 401       | The Privy / Dynamic identity token failed verification (bad signature, wrong audience, expired).                                                                          |
| `token_required`          | 401       | The session-create request arrived without an Authorization header.                                                                                                       |
| `origin_not_allowed`      | 403       | Request `Origin` isn't in the credential's allowlist. Check your Calm dashboard.                                                                                          |
| `wallet_not_linked`       | 403       | The wallet on the session request isn't among the Privy/Dynamic user's linked accounts. Often the identity-token toggle is off, so the wallet didn't ride inside the JWT. |
| `publishable_key_missing` | 400       | No publishable key reached the API — usually a bundler stripping the env var. Check `NEXT_PUBLIC_CALM_KEY`.                                                               |
| `invalid_publishable_key` | 400 / 401 | `calmKey` is malformed (`400`) or unknown / revoked (`401`). Check your Calm dashboard.                                                                                   |
| `binding_expired`         | 401       | The SIWE nonce expired (5-minute TTL) before the user signed. Re-mount or retry.                                                                                          |
| `siwe_invalid`            | 401       | The signed SIWE message failed verification — wrong nonce, wrong recovered address, time bounds, or unparseable input.                                                    |

### Wallet

| code               | status | when                                                                                                                                                                                                                                                     |
| ------------------ | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_required` | 401    | No `calm_session` cookie on a cookie-credentialed read. The SDK gates these reads on the session being live, so seeing this in a hook's `error` field usually means the session was cleared mid-render or the provider mounted without a wallet address. |
| `session_invalid`  | 401    | The session cookie was present but failed verification.                                                                                                                                                                                                  |
| `invalid_wallet`   | 400    | The `:address` path parameter isn't a valid Ethereum address.                                                                                                                                                                                            |
| `validation_error` | 400    | Body parsed as JSON but failed zod validation. `message` carries the field-level details.                                                                                                                                                                |

## Retry behaviour

The SDK's hooks retry 5xx and network errors up to 3 times with
react-query's default backoff. 4xx is treated as terminal — codes like
`invalid_wallet` or `validation_error` describe a fixed state, not a
transient failure, so retrying just spams the console and the server.
