# 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 and `SyntaxError` on a body that is not JSON. |
| 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 (`SyntaxError`, see Validation Errors), 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 server token (`SERVER_TOKEN_*`, `SERVER_CREDENTIAL_*`), 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, still running, or of a request whose outcome is not known (`IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_OUTCOME_UNKNOWN`). |
| 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`. Also a signed transaction a node refused (`RPC_ERROR`) or a signature from another key (`WRONG_SIGNER`). |
| 423 | Frozen or quarantined (`LockedError`): the tenant, the user or the wallet is frozen, or the operation quarantined — `details.code` `TENANT_FROZEN`, `USER_FROZEN`, `WALLET_FROZEN` or `OPERATION_QUARANTINED`; or, for an institutional tenant, its policy froze the wallet (`POLICY_FROZEN`). It answers the same until it is lifted: don't retry. See [Freezes & Quarantines](https://docs.aureahub.com/docs/guide-freezes.md). |
| 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.

**A body that is not JSON → `400`.** With `Content-Type: application/json`, a body that does not parse is answered before any check, with `"error": "SyntaxError"` and the JSON parser's own message, which gives the position and its line and column. For the body `{"refreshToken": "abc"` (the closing brace missing):

```json
{
  "statusCode": 400,
  "error": "SyntaxError",
  "message": "Expected ',' or '}' after property value in JSON at position 22 (line 1 column 23)"
}
```

The wording comes from the runtime's parser and is not part of the API: show it to a developer, never parse it.

## 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`, `PREPARED_TX_SUPERSEDED` (`409`: a newer preparation for the same wallet and chain took its nonce), `PREPARED_TX_CANCELLED` (`409`: its operation was cancelled), `SIGNATURE_DECODE_FAILED`, `WRONG_SIGNER`, `GAS_CAP_EXCEEDED` (`422`: the prepared fee is above the cap), `NO_ROUTE_FOUND`, `NO_LIQUIDITY_POOL` and `CONTRACT_NOT_DEPLOYED`, and `RPC_ERROR` (`422`) when a node refuses a swap or an approval broadcast: the message is the node's, and `details.rpcCode` its error code.
- **Moving a custodial key to the user's device** ([Request Export Token](https://docs.aureahub.com/docs/wallets-export-token.md), [Export Encrypted Key](https://docs.aureahub.com/docs/wallets-export-key.md), [Migration Challenge](https://docs.aureahub.com/docs/wallets-migration-challenge.md), [Confirm Client Custody](https://docs.aureahub.com/docs/wallets-confirm-custody.md), [Cancel Migration](https://docs.aureahub.com/docs/wallets-cancel-migration.md)) refuses with a `BlockchainError` whose code is in `code` and in `details.code`: `KEY_ALREADY_MIGRATED` (`409`), `MIGRATION_PENDING` (`409`), `EXPORT_TOKEN_USED` and `EXPORT_TOKEN_EXPIRED` (`410`), `EXPORT_ALREADY_TRANSMITTED` (`409`). Each page lists which message goes with which code.

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` and `IDEMPOTENCY_KEY_REQUIRED` (`400`; the second for an institutional tenant's money route sent without a key), `IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_IN_PROGRESS` and `IDEMPOTENCY_OUTCOME_UNKNOWN` (`409`): see [Idempotency](https://docs.aureahub.com/docs/idempotency.md).
- `EARN_*`: the Earn products — see [Earn](https://docs.aureahub.com/docs/guide-earn.md).
- `WALLET_CHAIN_UNKNOWN` (`400`): a wallet made or registered on a chain Aurea does not know — see [Create Wallet](https://docs.aureahub.com/docs/wallets-create.md).
- `ACCOUNT_*` (`400`, `404`, `409`), `ACCOUNTS_BULK_DUPLICATE_REF` (`400`), `ACCOUNTS_BULK_NOT_FOUND` (`404`) and `ACCOUNTS_BULK_OFF` (`503`): your tenant's accounts — see [Accounts](https://docs.aureahub.com/docs/guide-accounts.md).
- `TENANT_FROZEN`, `USER_FROZEN`, `ACCOUNT_FROZEN`, `WALLET_FROZEN` and `OPERATION_QUARANTINED` (`423`), and `FREEZE_STATUS_UNREAD` (`503`, nothing was signed: retry): a freeze or a quarantine — see [Freezes & Quarantines](https://docs.aureahub.com/docs/guide-freezes.md). Placing and lifting them: `FREEZE_TARGET_NOT_FOUND`, `FREEZE_NOT_FOUND`, `OPERATION_NOT_FOUND` and `QUARANTINE_NOT_FOUND` (`404`), `ALREADY_FROZEN`, `FREEZE_NOT_ACTIVE`, `ALREADY_QUARANTINED`, `QUARANTINE_NOT_ACTIVE`, `OPERATION_NOT_PENDING` (with `details.state`) and `OPERATION_NOT_QUARANTINABLE` (with `details.reason`) (`409`), `FREEZE_SELF` and `FREEZE_LIFT_PLATFORM_ONLY` (`403`), `FREEZE_UNTIL_PAST` and `FREEZE_REASON_INVALID` (`400`).
- `SERVER_AUTH_SCHEMES_CONFLICT` (`400`), `SERVER_TOKEN_*` and `SERVER_CREDENTIAL_*` (`401`): a refused server token, and `SERVER_CREDENTIAL_ROLE_NOT_ALLOWED` (`403`, with `details.permission`) for a credential whose role does not reach the route; `SERVER_TOKEN_REQUIRED` (`401`): an institutional tenant's HMAC signature where a server token is accepted; `SERVER_CREDENTIAL_*` also on the credential routes (`400`, `403`, `404`, `409`), with `TENANT_NOT_FOUND` (`404`) — see [Server Credentials](https://docs.aureahub.com/docs/guide-server-credentials.md).
- `WEBHOOK_ENDPOINT_INVALID` (`400`, with `details.field`, and `details.reason` for an address), `WEBHOOK_ENDPOINT_LIMIT` (`409`), `WEBHOOK_ENDPOINT_NOT_FOUND` (`404`), `WEBHOOK_QUERY_INVALID` and `WEBHOOK_RESEND_INVALID` (`400`, with `details.field`): your tenant's webhook endpoints, deliveries and resends — see [Events to Your Server](https://docs.aureahub.com/docs/guide-events.md).
- `POLICY_*`: your tenant's policy sets, and — for an institutional tenant — the refusals of its policy when something would be signed: `POLICY_BLOCKED` and `POLICY_APPROVAL_REQUIRED` (`403`), `POLICY_FROZEN` (`423`), `POLICY_SCREENING_UNAVAILABLE` and `POLICY_UNAVAILABLE` (`503`), and, when Aurea enables designated signers, `POLICY_SIGNER_NOT_DESIGNATED` (`403`) — see [Policy Sets](https://docs.aureahub.com/docs/guide-policy-sets.md).
- `GROUP_*` (`400`, `404`, `409`): your tenant's groups of users — see [Groups](https://docs.aureahub.com/docs/guide-groups.md).
- `ADDRESS_BOOK_*` (`400`, `403`, `404`, `409`, `410`, `422`, `503`): your tenant's address book — and, where Aurea enables it for your tenant, a destination outside it: `ADDRESS_BOOK_DESTINATION_NOT_ALLOWED` and `ADDRESS_BOOK_DESTINATION_UNRESOLVED` (`403`), `ADDRESS_BOOK_UNAVAILABLE` (`503`) — see [Address Book](https://docs.aureahub.com/docs/guide-address-book.md).
- `MFA_REQUIRED` (`401`, the login's answer with an `mfaToken`), `MFA_ENROLLMENT_REQUIRED` and `MFA_NOT_AVAILABLE` (`403`), `MFA_CHALLENGE_INVALID` (`401`), `MFA_CODE_INVALID` (`401` at the second step, `400` elsewhere), `MFA_OFF`, `MFA_NO_PENDING`, `MFA_NOT_ENROLLED` and `MFA_REMOVAL_REFUSED` (`409`): two-factor sign-in — see [Two-Factor Sign-In](https://docs.aureahub.com/docs/guide-two-factor.md).
- `CURRENT_PASSWORD_REQUIRED` (`400`), `CURRENT_PASSWORD_INVALID` (`403`) and `CURRENT_PASSWORD_UNAVAILABLE` (`409`): changing one's own password or email where Aurea has your tenant ask for the current password — see [Update User](https://docs.aureahub.com/docs/users-update.md).
- `QUORUM_*` (`400`, `403`, `404`, `409`), and the `202` `{ "state": "awaiting_quorum", "requestId" }` of an administrator's action held for another administrator: your tenant's quorum — see [Your Tenant's Quorum](https://docs.aureahub.com/docs/guide-tenant-quorum.md).
- `PERSONAL_SIGN_NOT_ALLOWED`, `MESSAGE_DOMAIN_NOT_ALLOWED`, `RAW_SIGN_NOT_ALLOWED`, `APPROVAL_SPENDER_NOT_ALLOWED` and `SWAP_ROUTER_NOT_ALLOWED` (`403`), `APPROVAL_AMOUNT_REQUIRED` (`400`), `APPROVAL_RESET_REQUIRED`, `SWAP_QUOTE_UNKNOWN`, `TX_NOT_SIMULATED` and `TX_SIMULATION_DIVERGES` (`409`): the signing rules and the transaction simulation, where Aurea enables them for your tenant — see [Signing Rules](https://docs.aureahub.com/docs/guide-signing-rules.md).
- `APPROVAL_*` (`400`, `403`, `404`, `409`): approval keys and decisions — see [Approvals](https://docs.aureahub.com/docs/guide-approvals.md) — except the three token-approval refusals of the signing rules above.
- `OPERATION_NOT_FOUND` (`404`): an operation you may not read, or none — see [Operations and Fees](https://docs.aureahub.com/docs/guide-operations.md). Cancelling one: `OPERATION_CANCEL_FORBIDDEN` (`403`), `OPERATION_ALREADY_BROADCAST`, `OPERATION_FINAL` and `OPERATION_IN_PROGRESS` (`409`, with `details.state`), and `OPERATION_CANCELLED` (`409`, with `details.operationId`) at the broadcast of what it prepared — see [Cancel an Operation](https://docs.aureahub.com/docs/operation-cancel.md). Speeding one up or dropping it: `OPERATIONS_RBF_OFF` and `OPERATION_RBF_FORBIDDEN` (`403`), the `OPERATION_*` and `OPERATION_RBF_*` refusals (`409`) — see [Speed Up](https://docs.aureahub.com/docs/operation-speed-up.md). `FEE_CHAIN_NOT_CONFIGURED` and `WALLET_NOT_FOUND` (`404`), `FEE_ASSET_INVALID` (`400`) and `FEE_ESTIMATE_UNAVAILABLE` (`502`): the fee, spendable-amount and address routes.
- `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, the second step of two-factor sign-in, Google and Apple sign-in, and **60 per minute** each for refresh and logout. 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.

> 💡 Every response carries the request's ID in `X-Request-ID`, and browsers may read it. You can send your own `x-request-id` header: the API keeps it when it is 8 to 64 letters, digits, `.`, `_` or `-`, and uses an ID of its own otherwise. Quote it when reporting a problem.

---

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