# Error codes

Every error response carries a `code` next to its `message`. The only exception
is the token endpoint, which keeps the OAuth shape (see [Errors](/errors)).

```json
{
  "statusCode": 404,
  "error": "Not Found",
  "code": "USER_NOT_FOUND",
  "message": "User not found"
}
```

Branch on `code`, not on `message`. A code is upper snake case and does not
change once published. A message is written for people, and its wording can
change at any time. Some codes add more fields to the body, such as
`INSUFFICIENT_BALANCE`; each one's row below says so.

Some codes appear with more than one status, for example `USER_NOT_FOUND` as a
`400` on construct calls and a `404` on user reads. Treat the code as the
answer, and use the status only to decide whether to retry (see
[Retries](/errors#retries)).

New codes can be added at any time. Handle a code you do not recognise by its
HTTP status.

## General

These can come from any endpoint. When an error has no more specific code, it
gets the general code for its status.

| Code | Status | Meaning |
| --- | --- | --- |
| `BAD_REQUEST` | `400` | The request was refused, and no more specific code applies |
| `VALIDATION_FAILED` | `400` | One or more fields failed validation. `message` joins every failure; the extra `errors` array lists them one per entry |
| `INVALID_PARAMETER` | `400` | A path parameter is malformed, for example a `userId` that is not a UUID |
| `INVALID_CURSOR` | `400` | A pagination `cursor` was not issued by this API, or was altered |
| `INVALID_PAGE` | `400` | `page` is below `1` |
| `INVALID_AMOUNT` | `400` | An amount is not a positive number, or cannot be parsed |
| `AMOUNT_TOO_SMALL` | `400` | An amount is below the token's smallest unit |
| `UNSUPPORTED_CHAIN` | `400` | The chain is not supported for this operation |
| `REQUEST_TOO_DEEP` | `400` | The JSON body is nested too deeply |
| `UNAUTHORIZED` | `401` | Authentication failed, and no more specific code applies |
| `FORBIDDEN` | `403` | The caller is not allowed to do this |
| `NOT_FOUND` | `404` | No such route or resource |
| `CONFLICT` | `409` | The request conflicts with existing data |
| `PAYLOAD_TOO_LARGE` | `413` | The request body is too large |
| `TOO_MANY_REQUESTS` | `429` | Rate limited. Back off and retry |
| `INTERNAL_SERVER_ERROR` | `500` | Something failed on our side. Retry with backoff |
| `NOT_IMPLEMENTED` | `501` | The operation exists but is not available yet |
| `SERVICE_UNAVAILABLE` | `503` | A dependency is unavailable. Retry with backoff |

## Authentication

Returned on every `/api/v1/s2s/…` route except the token endpoint. Each `401`
also carries a `WWW-Authenticate: Bearer` challenge.

| Code | Status | Meaning |
| --- | --- | --- |
| `ACCESS_TOKEN_MISSING` | `401` | No `Authorization: Bearer` header was sent |
| `ACCESS_TOKEN_INVALID` | `401` | The token is malformed, expired, or was not issued by us. Fetch a new one |
| `CREDENTIAL_UNKNOWN` | `401` | The credential that issued the token no longer exists |
| `CREDENTIAL_REJECTED` | `401` | The credential is revoked or expired, or your organisation is suspended. Fetching a new token will not help |
| `SOURCE_IP_NOT_ALLOWED` | `401` | The request came from an address that is not on the credential's IP allowlist |
| `S2S_NOT_CONFIGURED` | `401` | The partner API is not enabled on this environment |
| `SCOPE_MISSING` | `403` | The token is valid but lacks a scope this route needs. `message` names the scope |

## Users

| Code | Status | Meaning |
| --- | --- | --- |
| `USER_NOT_FOUND` | `400`, `404` | No such user, **or** the user belongs to another partner (see [`404` on a user](/errors#404-on-a-user)) |
| `WALLET_NOT_LINKED` | `400` | The user has no wallet address yet. Link one before building a transaction |
| `WALLET_ALREADY_LINKED` | `400` | The user already has a wallet. A linked wallet cannot be replaced |
| `EMAIL_ALREADY_REGISTERED` | `409` | Another user already has this email |
| `WALLET_ALREADY_REGISTERED` | `409` | Another user already has this wallet address |
| `USER_ALREADY_REGISTERED` | `409` | The email or wallet address was registered by a concurrent request |
| `UNKNOWN_COUNTRY_CODE` | `400` | `countryCode` is not a country code we recognise |

## Staking

| Code | Status | Meaning |
| --- | --- | --- |
| `POOL_NOT_FOUND` | `400`, `404` | No pool with that slug or ID |
| `POOL_DISABLED` | `403` | The pool is turned off for your partner account |
| `STAKING_DISABLED` | `400` | The pool is not accepting new stakes right now |
| `POOL_MISCONFIGURED` | `400` | The pool cannot build transactions right now. Contact us |
| `MAX_AMOUNT_NOT_SUPPORTED` | `400` | `maxAmount: true` is not available for unstaking from this pool type |
| `UNSTAKE_NOT_SUPPORTED` | `501` | Unstaking is not available for this pool type yet |
| `INSUFFICIENT_POOL_LIQUIDITY` | `400` | The pool cannot pay out this much right now. `message` gives the most it can pay |
| `TRANSACTION_BUILD_FAILED` | `400` | The deposit or withdrawal could not be built. Retry, and contact us if it keeps failing |

## Stocks

| Code | Status | Meaning |
| --- | --- | --- |
| `STOCK_NOT_FOUND` | `404` | No stock or commodity with that symbol in the catalogue |
| `STOCK_DISABLED` | `403` | The stock is turned off for your partner account |
| `STOCK_TRADING_DISABLED` | `400` | Buying or selling this stock is turned off right now. `message` says which side |
| `MARKET_DATA_UNAVAILABLE` | `400` | No price or market data is available for the stock right now. Retry later |
| `MARKET_DATA_NOT_FOUND` | `404` | The market data provider has no data for this symbol |
| `MARKET_DATA_PROVIDER_ERROR` | `400`, `500` | The market data provider refused the request or failed |
| `SWAP_PROVIDER_ERROR` | `400`, `500`, `502` | The swap provider could not quote or build the trade. `502`: it returned a transaction that failed our checks; retry later |
| `STOCK_MISCONFIGURED` | `500` | The stock is set up incorrectly on our side. Contact us |

## Bridge

| Code | Status | Meaning |
| --- | --- | --- |
| `BRIDGE_ROUTE_NOT_FOUND` | `404` | No route between that pair of chains and tokens |
| `BRIDGE_ROUTE_PAUSED` | `400` | The route exists but is paused |
| `BRIDGE_AMOUNT_BELOW_MINIMUM` | `400`, `503` | The amount is below the route's minimum, or too low for the provider to relay |
| `BRIDGE_AMOUNT_ABOVE_MAXIMUM` | `400` | The amount is above the route's maximum |
| `BRIDGE_FEE_TOO_HIGH` | `503` | The provider's fee is above the route's ceiling right now. Retry later |
| `BRIDGE_QUOTE_EXPIRED` | `400`, `503` | The quote expired, or would expire before it could be signed. Build a new transaction |
| `BRIDGE_TRANSFER_NOT_FOUND` | `404` | No transfer with that ID for this user |
| `BRIDGE_TRANSFER_NOT_PENDING` | `400` | The transfer was already submitted or settled. `message` gives its status |
| `BRIDGE_PROVIDER_UNAVAILABLE` | `503` | The bridge provider is unreachable or returned something unusable. Retry with backoff |

## Transactions

Returned by every construct and send call (stake, unstake, buy, sell and
bridge).

| Code | Status | Meaning |
| --- | --- | --- |
| `INSUFFICIENT_BALANCE` | `400` | The wallet cannot afford the batch with the platform fee included. Extra fields: `token`, `required`, `available`, `shortfall` (see [Insufficient balance](/errors#insufficient-balance)) |
| `INTENT_NOT_FOUND` | `400` | The `intentId` has expired, was already submitted, was built for another user, or was built by another product's construct call (send a bridge batch to the bridge `send`, a stock trade to the stocks `send`). Build a new one; do not retry |
| `AUTHORIZATION_INVALID` | `400` | The EIP-7702 authorization was not signed by the wallet that sends the batch, or its `yParity` is invalid, or the send carried an `authorization` when `bundle.authorization` was `null` (or left it out when it was not) |
| `BATCH_EMPTY` | `400` | The batch has no calls |
| `GAS_SPONSORSHIP_UNAVAILABLE` | `400` | Gas for this transaction cannot be sponsored right now |
| `BUNDLER_ERROR` | `400`, `503` | `400`: the bundler rejected the user operation; `message` ends with the ERC-4337 code when there is one, e.g. `(AA24)` for a bad signature. `503`: the bundler could not be reached or timed out; retry the same `send` |
| `BATCH_SEND_IN_PROGRESS` | `409` | Another `send` for this `intentId` is still running. Wait for it rather than retrying |
| `BATCH_REVERT_LIMIT` | `429` | Too many of this user's recent transactions reverted on-chain (for example, funds moved out before the batch landed). Try again later |
| `CHAIN_NOT_CONFIGURED` | `400`, `503` | The chain is not set up for transactions on this environment |
| `TOKEN_NOT_CONFIGURED` | `400` | A token in the batch has no contract address on the chain |

## Live stock prices stream

The [stream](/stream) uses the same convention on its `error` messages:

| Code | Meaning |
| --- | --- |
| `BAD_REQUEST` | The message was not a JSON object, was sent as a binary frame, or had a missing or invalid field |
| `UNKNOWN_CHANNEL` | No channel has that name |
| `UNAVAILABLE` | The channel cannot serve right now. Retry later |
| `INTERNAL_ERROR` | Something failed on our side. Retry later |
