# Handling exceptions (v1)

> Every v1 error shape, what each exception means, and which errors to retry.

> API v1 is deprecated. It stays supported, but new integrations should use the [Khalani API](/). See [Migrate to v2](/v1/migrate/).

Treat errors as part of the user experience: an expired quote, an unsupported pair and a refund are all ordinary outcomes. Check the HTTP status first, then the body.

## Error bodies

Most errors are an exception envelope. Branch on `name`:

```json
{
  "message": "Quote(quoteId: 3f6c2a1e-8b4d-4e7a-9c15-2d0b7e9a4f61, routeId: Hyperstream) not found",
  "name": "QuoteNotFoundException",
  "details": { "quoteId": "3f6c2a1e-8b4d-4e7a-9c15-2d0b7e9a4f61", "routeId": "Hyperstream", "code": 0 }
}
```

| Field | Type | Meaning |
|---|---|---|
| `message` | string | Human-readable. Log it; do not parse it. |
| `name` | string | The exception. Use it for logic. |
| `details` | object or array | Extra context. Optional, and hidden on some exceptions. |

A few responses have other shapes:

| Source | Status | Body |
|---|---|---|
| Request validation | `400` | `{ "message": "Validation failed", "name": "ValidationException", "details": [{ "field", "message", "code" }] }` |
| A body that is not JSON | `400` | `{ "message": "Malformed JSON body", "name": "BadRequestException" }` |
| Rate limit (build only) | `429` | `{ "errorCode": "RATE_LIMITED", "message": "Too many requests, please retry later" }` |
| Maintenance | `503` | `{ "name": "MaintenanceException", "message": "SystemMaintenance" }`, with `Retry-After: 300` |
| Unknown order in `GET /v1/orders/by-id/{orderId}` | `404` | Plain text: `Order not found` |
| Unknown path | `404` | Plain text: `404 Not Found` |

So read the body as text, then parse it as JSON when you can. Log `name ?? errorCode` with `message`, and keep the `x-request-id` header of every failed response.

## What to retry

1. **A `4xx` is not retried as it is.** Fix the request, or quote again. The one exception is `429`: back off and retry.
2. **A `503` is maintenance.** Wait for `Retry-After` (300 seconds) and retry. Quotes and builds pause together; submits and order reads keep working.
3. **A `500` can be retried with backoff,** except a submit that says the deposit was submitted. See the matrix.
4. **An expired or missing quote is not retried.** Quote again and build from the new quote.

For the user, map errors to an action: "Price expired, refresh", "This token is not supported on that chain", "Connect the wallet you quoted with".

## Streaming quotes

With `POST /v1/quotes?mode=stream`:

- An invalid body returns an ordinary JSON error, not a stream.
- A route that fails or does not quote is left out. The stream can have fewer lines than there are routes, or none.
- The stream ends early when the native route quotes. Its line is the only one sent, and the external routes are dropped. With `?routes=`, every listed route streams, and the stream ends when all of them have answered or failed.

## Exception matrix

| `name` | Status | Retry? | Typical cause | What to do |
|---|---:|---|---|---|
| `ValidationException` | 400 | No | A field is missing or malformed. | Fix it. `details[]` names each field. |
| `BadRequestException` | 400 | No | Build: the quote expired (`Quote has expired. Please request a new quote.`), `from` is missing or differs from the quote's `fromAddress`, the quote is bound to another wallet, or `recipient` or `refundTo` differs from an earlier build. Submit: the hash format does not match the chain, the `buildId` is missing or stale, or the quote is funded through a deposit address. | Quote again for an expired quote. Otherwise fix the request. |
| `CannotFillException` | 400 | No | The route cannot build this deposit. `details` is hidden. | Quote again, or choose another route. |
| `UnexpectedFromAddressException` | 400 | No | The address does not fit the source chain's family, on an external route. | Use an address of the source chain's family. |
| `NotSupportedContractException` | 400 | No | A contract the route needs is not configured. | Choose another route. |
| `BuildDepositParsingException` | 400 | No | The route returned a plan the API could not validate. | Quote again and build once more, then try another route. |
| `NotSupportedDepositMethodException` | 400 | No | The `depositMethod` is not one the route offers. `details` holds `depositMethod`, `fillerId` and `supportedMethods`. | Pick from `supportedMethods`, or leave `depositMethod` out. |
| `BroadcastException` | 400 | No | Server-side broadcast failed: balance, nonce or an expired Solana blockhash. `details` is hidden. | Fix the cause, sign again and resubmit. |
| `NotSupportedTokenException` | 404 | No | Build: the token is not supported on that chain. | Use a token from [`GET /v1/tokens`](/v1/reference/tokens/). |
| `NotSupportedChainException` | 404 | No | Build: the chain is not supported. Submit: the API cannot broadcast on that chain. | Use a chain from [`GET /v1/chains`](/v1/reference/chains/), or broadcast yourself and submit the hash. |
| `NotSupportedAssetReverseContractException` | 404 | No | The native route has no deposit contract on that chain. | Choose another route. |
| `IntentNotFoundException` | 404 | No | The stored quote has no intent to build from. | Quote again. |
| `QuoteNotFoundException` | 404 | No | Unknown `quoteId` and `routeId` pair, a quote past its storage window (`validBefore` plus 60 seconds), or a quote already used by a successful submit. | Quote again. After a submit that may have succeeded, look the order up with `GET /v1/orders/{address}` before doing anything else. |
| `RATE_LIMITED` (`errorCode`) | 429 | Yes | Too many builds. See [Limits and reliability](/v1/limits/). | Back off and retry. There is no `Retry-After`. |
| `InternalErrorException` | 500 | Yes | An unhandled server error. | Retry with backoff, then contact Khalani with the `x-request-id`. |
| `MaintenanceException` | 503 | Yes | Quotes and builds are paused. | Retry after `Retry-After`. |

A quote that no route can serve is not an error: `POST /v1/quotes` returns `200` with an empty `routes[]`. Unsupported tokens and chains only raise exceptions at the build.

:::caution[A 500 after the deposit]
A submit can return `500` with a message that starts `Deposit was submitted, but`. The deposit is on-chain; only the API's record failed. Do not send the deposit again. Look the order up by address, and contact Khalani with the `txHash` if it is missing. The same applies when two submits for one deposit race: one creates the order and the other can fail with `500`.
:::

`DuplicateRecordException` (`409`) exists in the API, but a duplicate submit does not reach it. It returns `404 QuoteNotFoundException` instead, as above.
