# Error Handling

Errors come back as JSON with an HTTP status code. Most use a common envelope; this page covers the envelope, the documented exceptions, and how to handle both.

## Error Format

Errors raised by the API use this envelope:

```json
{
  "statusCode": 401,
  "error": "UnauthorizedError",
  "message": "Authentication required"
}
```

| Field | Type | Description |
| --- | --- | --- |
| statusCode | number | The HTTP status, repeated in the body. |
| error | string | For errors raised by the API, the error class name: `BadRequestError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `ValidationError`, `InternalServerError`, `ServiceUnavailableError`, `BlockchainError` or `NoahApiError`. Request-validation and database errors use a label instead, such as `Bad Request`, `Validation Error` or `Conflict`. Other failures carry the underlying error name, for example `Error` on `429` responses. |
| message | string | Human-readable description. Branch on the status code, and on `code` where present, rather than on this text. |
| details | any | Optional. Present on validation errors and on some application errors; the shape depends on the error. |
| code | string | Optional. A machine-readable code, present only on some errors; see Error Codes below. |

Unexpected server failures return `500` with `"message": "Internal server error"`; internal details are not included.

## Response Variations

- **Endpoint response schemas filter error bodies.** When an endpoint declares a response body for an error status, only the declared fields are sent. The sign-in endpoints declare `{ error, message }` for several statuses, so a failed login returns just `{ "error": "UnauthorizedError", "message": "Invalid credentials" }`, with no `statusCode` and no `details`. Each auth endpoint page lists which statuses are affected.
- **Some endpoints write their own body.** For example, the [password-reset](https://docs.aureahub.com/docs/auth-forgot-password.md) endpoints return `{ "error": "…" }`.
- **Unknown routes** return the framework's not-found body: `{ "message": "Route GET:/v1/unknown not found", "error": "Not Found", "statusCode": 404 }`.

So always read the HTTP status first, and take the text from `message`, falling back to `error`.

## HTTP Status Codes

| Status | Meaning |
| --- | --- |
| 400 | The request failed schema validation (`Request validation failed`), the JSON body could not be parsed, the endpoint rejected a value (`BadRequestError`), or a referenced record does not exist (`Invalid reference`). |
| 401 | Missing or invalid bearer token (`Authentication required`), a failed tenant signature, or rejected credentials or refresh token. See [Authentication](https://docs.aureahub.com/docs/authentication.md). |
| 403 | Authenticated, but not allowed to perform this action (`ForbiddenError`). |
| 404 | The resource was not found (`NotFoundError`), `x-tenant-api-key` does not match an active tenant (`Invalid API key`), or the route does not exist. |
| 409 | Conflict with existing data (`ConflictError`), a duplicate record (`Resource already exists`), `PRIMARY_WALLET_EXISTS`, or an `Idempotency-Key` used for another request or still running (`IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_IN_PROGRESS`). |
| 422 | The input passed schema validation but failed the endpoint's own checks: detailed validation (`Request validation failed` with `details`) or a `ValidationError` such as `Google account email is not verified`. |
| 429 | A rate-limited route received too many requests. See Rate Limits below. |
| 500 | Unexpected server failure (`Internal server error`). |
| 502 | A service the API calls refused Aurea's connection for your tenant or failed unexpectedly, for example `NoahApiError` with `details.code` `NOAH_AUTHENTICATION_FAILED`. It is not a problem with the user's session: don't sign the user out. |
| 503 | A service the API depends on is temporarily unavailable (`ServiceUnavailableError`), or the bank ramp is unavailable, slow or rate limiting (`NoahApiError` with `NOAH_UNAVAILABLE`, `NOAH_TIMEOUT`, `NOAH_UNREACHABLE` or `NOAH_RATE_LIMITED`). `NoahApiError` with `NOAH_NOT_CONFIGURED` means Aurea has not connected the bank ramp for your tenant in the environment the call selected, so the bank ramp was not called; retrying doesn't help until Aurea connects it (Bank Ramp Setup). |

Some blockchain operations use other statuses, for example `501` when a required contract is not available on the network.

## Validation Errors

Input is checked in two stages, and the two stages report problems differently.

**Schema validation → `400`.** Runs before the endpoint's logic, and before the tenant signature check. `details` lists the validator's errors:

```json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Request validation failed",
  "details": [
    {
      "instancePath": "",
      "schemaPath": "#/required",
      "keyword": "required",
      "params": { "missingProperty": "refreshToken" },
      "message": "must have required property 'refreshToken'"
    }
  ]
}
```

**Endpoint checks → `422`.** Rules applied inside the endpoint, such as password strength at registration. `details` lists each failing field path with a message:

```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Request validation failed",
  "details": [
    { "path": "password", "message": "Password must contain at least one uppercase letter" },
    { "path": "password", "message": "Password must contain at least one digit" }
  ]
}
```

> ℹ️ Where an endpoint declares `{ error, message }` for `400` (for example register, login, Google and Apple sign-in), `details` is not returned for schema-validation failures.

## Error Codes

Most errors have **no** `code`. The API's error handler adds one in these cases:

- `PRIMARY_WALLET_EXISTS` (`409`): a primary wallet already exists for this chain and network type. Set an existing wallet as primary, or create the new wallet as non-primary.
- **Blockchain-operation errors** (`"error": "BlockchainError"`) carry a `code` and a boolean `retryable`, and may add `suggestedAction` and `fallbackAvailable`. Codes raised include `PREPARED_TX_NOT_FOUND`, `PREPARED_TX_ALREADY_USED`, `PREPARED_TX_EXPIRED`, `SIGNATURE_DECODE_FAILED`, `WRONG_SIGNER`, `NO_ROUTE_FOUND`, `NO_LIQUIDITY_POOL` and `CONTRACT_NOT_DEPLOYED`.

A few application errors carry their code in `details.code` instead:

- `CARD_TOPUP_UNAVAILABLE` (`403`): card top-up is switched off. Top up by bank transfer with [Initiate Deposit](https://docs.aureahub.com/docs/payin-initiate.md).
- `PAYOUT_PRODUCTION_UNAVAILABLE` (`403`): a production payout was requested. Payouts are available only with `isTestnet: true`, and no funds are moved.
- `NOAH_FUNCTION_OFF` (`403`): your tenant has not switched on that the bank ramp function (`details.function`: `kyc`, `payin` or `payout`) in that environment (`details.environment`). See [Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md).
- `NOAH_PAIR_UNAVAILABLE` (`400`): the currency, or the currency and network, is not one your tenant offers with the bank ramp in that environment; `details.available` lists what it offers. See [Currencies & Networks](https://docs.aureahub.com/docs/bank-currencies.md).
- `NOAH_CUSTOMER_NOT_FOUND` (`404`, with `details.environment`): the user has no bank ramp profile in that environment yet. See [Onboarding Session](https://docs.aureahub.com/docs/payin-session.md).
- `NOAH_KYC_NOT_APPROVED` (`422`, with `details.onboardingStatus` and `details.kycStatus`): a payout quote, and a payout paid from the user's wallet, need a completed the bank ramp's onboarding and an approved KYC. See [Onboarding Session](https://docs.aureahub.com/docs/payin-session.md).
- `NOAH_CHANNEL_NOT_SUPPORTED` (`400`): a payout quote goes through a bank or identifier channel, never a card channel. `NOAH_QUOTE_NOT_FOUND` (`404`, with `details.environment`) and `NOAH_QUOTE_READY` (`409`): see [Answer a Form Step](https://docs.aureahub.com/docs/payout-quote-step.md).
- `NOAH_QUOTE_NOT_LOCKED` (`409`, with `details.quoteId` and `details.reason`: `not_ready` or `expired`), `NOAH_QUOTE_USED` (`409`, with `details.quoteId` and `details.payoutId`) and `NOAH_PAYOUT_SOURCE_BUSY` (`409`, with `details.network` and `details.cryptoCurrency`): paying a quote from the user's own wallet — see [Pay from a Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md).
- `NOAH_PAYOUT_AMOUNT_TOO_SMALL` (`400`, with `details.quoteId`): the payout quote leaves the beneficiary nothing — below the channel's `cryptoLimits.min` its fixed fee takes the whole payout, and the bank ramp prices it at zero rather than refusing it. Ask for a quote above that minimum. See [Pay from a Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md).
- `NOAH_SOURCE_NOT_ALLOWED` (`400`, with `details.environment` and `details.reason`: `wallet_not_found`, `watch_only`, `wrong_network` or `address_not_proven`): the wallet or proven address named as the source of a payout cannot send it; the answer never repeats the address. `NOAH_PAYOUT_NOT_FOUND` (`404`, with `details.environment`): see [Get a Wallet Payout](https://docs.aureahub.com/docs/payout-wallet-get.md).
- `details.outcome` `unknown`, beside `details.payoutId` on a `502` or `503` from [Pay from a Wallet](https://docs.aureahub.com/docs/payout-wallet-pay.md): Aurea cannot tell whether the bank ramp made the payout rule. Read the payout instead of sending the request again.
- `NOAH_NOT_CONFIGURED` (`503`): Aurea has not connected the bank ramp for your tenant in that environment, and the bank ramp was not called. See [Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md).
- `NOAH_MODE_OFF` (`403`): your tenant has not switched on that integration mode (`details.mode`) in that environment (`details.environment`) — the standalone mode, which proving an address needs and a payout from a proven address needs, or the Aurea wallets mode, which a payout from one of the user's wallets needs. See [Tenant Settings](https://docs.aureahub.com/docs/bank-settings.md).
- `NOAH_DESTINATION_NOT_ALLOWED` (`400`): a pay-in is delivered only to one of the user's Aurea wallets, with the Aurea wallets mode, or to an address the user proved, with the standalone mode. See [Initiate Deposit](https://docs.aureahub.com/docs/payin-initiate.md).
- `NOAH_ADDRESS_INVALID` (`400`), `NOAH_ADDRESS_CHALLENGE_LIMIT` (`409`), `NOAH_ADDRESS_SIGNATURE_INVALID` (`400`, with `details.reason` and `details.attemptsLeft`), `NOAH_ADDRESS_CHALLENGE_BURNT` and `NOAH_ADDRESS_CHALLENGE_EXPIRED` (`400`), `NOAH_ADDRESS_CHALLENGE_USED` (`409`): proving an address — see [Ask for a Challenge](https://docs.aureahub.com/docs/bank-address-challenge.md) and [Verify an Address](https://docs.aureahub.com/docs/bank-address-verify.md).
- `NOAH_ENVIRONMENT_MISMATCH` (`400`): the `isTestnet` sent to Initiate Deposit disagrees with the network's environment.
- `NOAH_WEBHOOK_INVALID` (`400`, with `details.field`, and `details.reason` for an address), `NOAH_WEBHOOK_CONFLICT` (`400`) and `NOAH_WEBHOOK_NOT_FOUND` (`404`): your tenant's webhooks — see [Save a Webhook](https://docs.aureahub.com/docs/tenant-webhook-save.md).
- `RETURN_URL_NOT_ALLOWED` (`400`): the return URL is not one your tenant allows. Ask the Aurea operator to add it — see [Bank Ramp Setup](https://docs.aureahub.com/docs/bank-setup.md).
- `RETURN_URL_NOT_HTTPS` (`400`): an onboarding session's return URL must be `https://`. `NOAH_CUSTOMER_TYPE_MISMATCH` (`409`, with `details.customerType`): The bank ramp already has the user with the other customer type. See [Onboarding Session](https://docs.aureahub.com/docs/payin-session.md).
- `IDEMPOTENCY_KEY_INVALID` (`400`), `IDEMPOTENCY_KEY_REUSED` and `IDEMPOTENCY_IN_PROGRESS` (`409`): see [Idempotency](https://docs.aureahub.com/docs/idempotency.md).
- `CARD_ONRAMP_OFF` (`403`, with `details.environment`), `CARD_ONRAMP_PAIR_UNAVAILABLE` (`400`, with `details.available`), `CARD_ONRAMP_PAIR_NOT_SOLD_IN_CURRENCY` (`400`, with `details.sourceCurrencies`), `CARD_ONRAMP_DESTINATION_NOT_ALLOWED` and `CARD_ONRAMP_AMOUNT_INVALID` (`400`), `CARD_ONRAMP_WALLET_NOT_FOUND` and `CARD_ONRAMP_SESSION_NOT_FOUND` (`404`), `CARD_ONRAMP_SESSION_FAILED` (`409`, with `details.failureCode`), `CARD_ONRAMP_SESSION_CLOSED` (`409`, with `details.status`), `CARD_ONRAMP_LINK_NOT_FOUND` (`404`) and `CARD_ONRAMP_LINK_EXPIRED` (`410`): the card-to-crypto onramp — see [Card to Crypto](https://docs.aureahub.com/docs/guide-card-onramp.md).
- The payment provider's own refusals (`"error": "StripeOnrampError"`) carry `providerStatus`, `providerCode` and `providerRequestId` beside the code: `CARD_ONRAMP_CUSTOMER_UNSUPPORTED` (`403`), `CARD_ONRAMP_INVALID_REQUEST` (`400`), `CARD_ONRAMP_NOT_FOUND` (`404`), `CARD_ONRAMP_DISABLED`, `CARD_ONRAMP_MERCHANT_NOT_SET_UP`, `CARD_ONRAMP_AUTHENTICATION_FAILED`, `CARD_ONRAMP_RATE_LIMITED` and `CARD_ONRAMP_NOT_CONFIGURED` (`503`), `CARD_ONRAMP_UPSTREAM_ERROR`, `CARD_ONRAMP_UNREACHABLE` and `CARD_ONRAMP_UNEXPECTED_RESPONSE` (`502`). The payment provider's message is never passed on.

If an endpoint declares an error body without `code`, response filtering removes it (see Response Variations).

## Rate Limits

Rate limits are set per route and counted per client IP; not every route is rate-limited. The auth routes allow **20 requests per minute** each for register, login, Google and Apple sign-in, and **60 per minute** for refresh. See [Rate Limiting](https://docs.aureahub.com/docs/rate-limiting.md).

Responses from a rate-limited route carry `x-ratelimit-limit`, `x-ratelimit-remaining` and `x-ratelimit-reset` (seconds until the window resets). Once the limit is exceeded, the route returns `429` with a `retry-after` header in seconds:

```json
{
  "statusCode": 429,
  "error": "Error",
  "message": "Rate limit exceeded, retry in 1 minute"
}
```

The wait named in the message depends on the route's window. Some endpoints apply their own throttling and body; for example, the password-reset endpoints return `429` with `{ "error": "Too many requests. Please try again later." }`.

## Handling Errors

```javascript
export async function parseApiResponse(res) {
  const body = await res.json().catch(() => null);
  if (res.ok) return body;

  const err = new Error(body?.message ?? body?.error ?? `HTTP ${res.status}`);
  err.status = res.status;      // always branch on this first
  err.code = body?.code;        // present only on some errors
  err.details = body?.details;  // validation errors, when returned
  if (res.status === 429) err.retryAfterSeconds = Number(res.headers.get('retry-after'));
  throw err;
}
```

- **401**: on bearer-protected routes, refresh the access token once and retry (see [Authentication](https://docs.aureahub.com/docs/authentication.md)). On signed routes, check the signature inputs.
- **400 / 422**: fix the request using `details` when it is returned. Resending the same input fails the same way.
- **429**: wait for `retry-after` seconds before retrying.

> 💡 You can send your own `x-request-id` header. The API uses it as the request ID in its logs, but does not echo it back in the response. Quote it when reporting a problem.

---

Web version: https://docs.aureahub.com/#errors
