# Errors & Troubleshooting

Every failure surface in peltier is typed. The REST API returns one JSON error shape; the SDK wraps it in `PeltierApiError`; the React hooks normalize everything (wallet, network, API) into `PeltierError`.

## REST error shape

Every error response body is:

```json
{ "error": "<human-readable message>" }
```

with a meaningful status:

| Status | Meaning |
| --- | --- |
| `400` | Bad request — invalid params, or a checkout commit with no pinned destination for that chain. |
| `401` | Missing, malformed, revoked, or wrong-type API key (carries `WWW-Authenticate: Bearer`). |
| `403` | Your key is valid but does not own the requested resource. |
| `404` | Not found — including resources owned by *another* merchant (IDOR mitigation: you can't distinguish "doesn't exist" from "not yours"). |
| `409` | Conflict — e.g. duplicate merchant email, or re-POSTing a subscription mandate whose digest already exists. Messages stay deliberately generic (no resource enumeration). |
| `410` | Gone — an inactive or expired payment link. |
| `429` | Per-IP rate limit exceeded — see [Rate Limits](/docs/rate-limits.md). |
| `500` | Internal error. |
| `502` | Upstream chain RPC / relay failure. The backend **never mutates state on this path** — 502 means "on-chain state unknown/unchanged", so the operation is safe to retry. |

## SDK: `PeltierApiError`

Every `@peltier/core` / `@peltier/node` API function throws `PeltierApiError` (never a bare `Error`):

```typescript
import { isPeltierApiError } from '@peltier/core'

try {
  await createPaymentSession(apiUrl, sk, { amount: '1000000' })
} catch (e) {
  if (isPeltierApiError(e)) {
    e.status    // HTTP status (0 for non-HTTP failures)
    e.endpoint  // the path that failed
    e.kind      // 'http' | 'network' | 'timeout' | 'parse'
    e.body      // the parsed { error } body, when the server sent one
  }
}
```

Every API function also accepts a trailing `RequestOptions` (`{ timeoutMs?, signal?, fetch? }`; default timeout 30 000 ms) — a blown deadline surfaces as `kind: 'timeout'`.

## Hooks: `PeltierError`

The React hooks (`useDepositSession`, `useSendFromWallet`, `useRequestSession`, `useStealthRequest`) expose `error: PeltierError | null` — `{ code, message, retryable, cause }`:

| Code | Meaning | Retryable |
| --- | --- | --- |
| `wallet_rejected` | The user declined the wallet prompt. | yes (re-prompt) |
| `wallet_error` | The wallet errored (not a rejection). | varies |
| `network_error` | Fetch/SSE transport failure. | yes |
| `api_error` | The backend returned an error (wraps a `PeltierApiError`, in `cause`). | per status |
| `verification_failed` | Client-side calldata verification failed closed (Flow C). | no |
| `commit_failed` | The detect-then-sign commit failed — call `retryCommit()`, never `reset()` (the burner is funded). | yes |
| `session_expired` | The session TTL lapsed before settlement. | no (new session) |
| `config_error` | Bad integration config (missing key, unknown decimals, missing `allowedRouters`). | no (fix code) |
| `unknown` | Anything else — `cause` carries the original. | — |

`toPeltierError(e)` normalizes any thrown value into this shape.

## Retry guidance

- **Retry:** `429` (back off first), `5xx`, `kind: 'network' | 'timeout'`. A `502` explicitly left state unchanged.
- **Don't retry:** other `4xx` — fix the request. A retried `POST /v1/payment_sessions` mints a **brand-new session** (no idempotency key — see [Idempotency](/docs/merchant-accounts.md)), so dedupe on your own `order_id`.

## Top integrator mistakes

1. **Missing `await` on webhook verification.** `verifyWebhookSignature` is **async** — without `await`, the returned Promise is truthy and every forged webhook passes. See [Webhooks](/docs/webhooks.md).
2. **Human-decimal amounts.** `createPaymentSession` amounts are integer **base units** (6 decimals): `'1000000'` = 1 USDC. A string containing `.` (or any non-digit) throws with an explanatory message. (The `@peltier/node` 402 gate's `price` is the documented exception — it takes human decimals.)
3. **Missing `styles.css`.** The web widgets render unstyled without `import '@peltier/react/styles.css'`.
4. **Unpinned destination.** A checkout commit is **rejected** unless `destinationAddress` / `destinationChainId` match the destination you pinned via `PUT /v1/destinations` — pin first, then mount the widget. See [Flow A](/docs/flow-a-checkout.md).
