> ## Documentation Index
> Fetch the complete documentation index at: https://nexus.gerowallet.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Handling

> Understanding and handling errors from the Gero Nexus API

The Gero Nexus API uses standard HTTP status codes and returns a consistent JSON error body to help you diagnose and handle issues gracefully.

## Error Response Format

Most errors return this JSON structure. Fields that don't apply to a given error are **omitted**
(null fields are not serialized), so a typical error has only the first six fields:

```json theme={null}
{
  "timestamp": "2026-06-23T15:30:00.123",
  "status": 404,
  "error": "Not Found",
  "message": "Transaction not found",
  "path": "/api/transactions/abc123",
  "requestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

| Field              | Type      | Always present | Description                                                                      |
| ------------------ | --------- | -------------- | -------------------------------------------------------------------------------- |
| `timestamp`        | string    | yes            | ISO-8601 server time when the error was produced                                 |
| `status`           | integer   | yes            | HTTP status code (mirrors the response status)                                   |
| `error`            | string    | yes            | Short error type, e.g. `"Bad Request"`, `"Not Found"`, `"Service Unavailable"`   |
| `message`          | string    | yes            | Human-readable description of what went wrong                                    |
| `path`             | string    | yes            | The request path that produced the error                                         |
| `requestId`        | string    | yes            | Unique ID for this error — include it when contacting support                    |
| `errorCode`        | string    | no             | Stable machine-readable code, present on specific conditions (see below)         |
| `validationErrors` | string\[] | no             | Field validation messages (`"field: message"`), present on 400 validation errors |
| `providerErrors`   | object    | no             | Upstream provider → message map, present on some 503 responses                   |

<Note>
  Branch on `status` (or the `errorCode` when present) for programmatic handling — not on the
  human-readable `message`, which may change. There is no `code` or `details` field.
</Note>

<Warning>
  **Rate-limit (429) and billing (402) responses use a different body.** A 429 is `{ "error":
      "rate_limit_exceeded", ... }`. A 402 has two variants: `{ "error": "subscription_required", ... }`
  (no active subscription) or `{ "error": "payment_required", "addon": ... }` (a paid add-on isn't
  active). See the 402 section below and [Rate Limits](/docs/concepts/rate-limits).
</Warning>

## HTTP Status Codes

### Success Codes

| Code  | Status     | Description                              |
| ----- | ---------- | ---------------------------------------- |
| `200` | OK         | Request successful                       |
| `201` | Created    | Resource created successfully            |
| `202` | Accepted   | Request accepted for processing          |
| `204` | No Content | Request successful, no content to return |

### Client Error Codes (4xx)

<AccordionGroup>
  <Accordion title="400 Bad Request" icon="xmark">
    The request was malformed or contains invalid parameters. Validation failures include a
    `validationErrors` array:

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 400,
      "error": "Bad Request",
      "message": "Validation failed",
      "path": "/api/blocks",
      "requestId": "a1b2c3d4-...",
      "validationErrors": ["getBlocks.page: Page must be at least 1"]
    }
    ```

    Query-parameter validation messages are prefixed with the controller method
    (`getBlocks.page`); request-body validation uses the bare field name (`page`).

    **Common causes:** missing/invalid parameters, invalid JSON body, a value out of range.

    **Solution:** check the request parameters against the endpoint's reference page.
  </Accordion>

  <Accordion title="401 Unauthorized" icon="lock">
    Authentication failed or was not provided.

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 401,
      "error": "Unauthorized",
      "message": "Invalid username or password",
      "path": "/api/auth/login",
      "requestId": "a1b2c3d4-..."
    }
    ```

    **Common causes:**

    * Missing or invalid API key
    * **API key sent in the wrong header** — an `nxs_` key must go in `X-Api-Key`, not
      `Authorization: Bearer` (see [Authentication](/docs/authentication))
    * Invalid or expired JWT session token

    **Solution:** verify your `X-Api-Key` header (or refresh your session token).
  </Accordion>

  <Accordion title="402 Payment Required" icon="credit-card">
    A 402 has **two variants** — discriminate on the `error` field (the body is flat, with no
    `errorCode`).

    **No active subscription** (`subscription_required`). Nexus has no free tier, so any account
    without an active subscription gets this on every gated data endpoint. No `addon` field:

    ```json theme={null}
    {
      "error": "subscription_required",
      "message": "An active subscription is required. Subscribe at https://nexus.gerowallet.io/dashboard/billing/"
    }
    ```

    **Add-on not active** (`payment_required`). You have a subscription, but the endpoint needs a
    paid add-on (Market Data, Wallet Analytics, Transaction Builder, IPFS). The `addon` field names it:

    ```json theme={null}
    {
      "error": "payment_required",
      "addon": "marketData",
      "message": "Upgrade required at https://nexus.gerowallet.io/dashboard/billing/"
    }
    ```

    **Solution:** subscribe or enable the add-on from your dashboard. See [Rate Limits](/docs/concepts/rate-limits#add-on-gating-402).
  </Accordion>

  <Accordion title="403 Forbidden" icon="ban">
    Authenticated, but not permitted for this action. Some 403s carry a stable `errorCode`
    (e.g. `EMAIL_NOT_VERIFIED`, `SUBSCRIPTION_REQUIRED`).

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 403,
      "error": "Subscription Required",
      "message": "An active subscription is required to access this resource",
      "path": "/api/keys",
      "requestId": "a1b2c3d4-...",
      "errorCode": "SUBSCRIPTION_REQUIRED"
    }
    ```

    **Solution:** check your plan/permissions, or upgrade.

    <Note>
      The 403 `SUBSCRIPTION_REQUIRED` `errorCode` comes from account-management actions (creating
      an API key, activating a subscription). A missing subscription on a **data** endpoint is
      instead a **402** with `error: "subscription_required"` and no `errorCode` (see above).
    </Note>
  </Accordion>

  <Accordion title="404 Not Found" icon="question">
    The requested resource does not exist.

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 404,
      "error": "Not Found",
      "message": "Transaction not found",
      "path": "/api/transactions/abc123",
      "requestId": "a1b2c3d4-..."
    }
    ```

    **Common causes:** invalid resource ID/hash, a typo in the path, or a transaction not yet
    confirmed on-chain.
  </Accordion>

  <Accordion title="429 Too Many Requests" icon="clock">
    Rate limit exceeded. This response uses the rate-limit body shape (not the standard error
    envelope) and there are two variants — a per-second throttle and a monthly-quota exhaustion.
    See [Rate Limits](/docs/concepts/rate-limits#when-you-hit-a-limit) for the full contract and how
    to tell them apart.
  </Accordion>
</AccordionGroup>

### Server Error Codes (5xx)

<AccordionGroup>
  <Accordion title="500 Internal Server Error" icon="server">
    An unexpected error occurred on the server. The `message` includes the `requestId`.

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 500,
      "error": "Internal Server Error",
      "message": "An unexpected error occurred. Please contact support with request ID: a1b2c3d4-...",
      "path": "/api/blocks",
      "requestId": "a1b2c3d4-..."
    }
    ```

    **Solution:** retry. If it persists, contact support with the `requestId`.
  </Accordion>

  <Accordion title="503 Service Unavailable" icon="wrench">
    The upstream data providers are unavailable (or the requested network isn't configured).
    Some 503s include a `providerErrors` map:

    ```json theme={null}
    {
      "timestamp": "2026-06-23T15:30:00.123",
      "status": 503,
      "error": "Service Unavailable",
      "message": "All providers are currently unavailable. Please try again later.",
      "path": "/api/blocks",
      "requestId": "a1b2c3d4-...",
      "providerErrors": { "KOIOS": "timeout", "BLOCKFROST": "503" }
    }
    ```

    **Solution:** retry after a brief delay. Check the [status page](https://nexus.gerowallet.io/status).
  </Accordion>

  <Accordion title="504 Gateway Timeout" icon="hourglass">
    A request to an upstream provider timed out.

    **Solution:** retry. For large queries, page through results with a smaller `pageSize`.
  </Accordion>
</AccordionGroup>

## Error Codes

`errorCode` is **optional** — it's present only on specific business/account conditions, not on
typical data-endpoint errors (most 400/404/503s omit it). When you do get one, it's stable and
safe to switch on. Examples you may encounter:

| `errorCode`                 | When                                                                                                                                         |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `EMAIL_NOT_VERIFIED`        | Login before the email address is verified                                                                                                   |
| `PASSWORD_AUTH_UNAVAILABLE` | Password login on an OAuth-only account                                                                                                      |
| `SUBSCRIPTION_REQUIRED`     | 403 on account-management actions (key creation, subscription activation) — distinct from the 402 `subscription_required` data-endpoint gate |
| `ACCOUNT_LOCKED`            | 423 Locked — too many failed logins (carries a `Retry-After` header)                                                                         |
| `API_KEY_LIMIT_EXCEEDED`    | Creating more API keys than your plan allows                                                                                                 |
| `PROVIDER_QUOTA_EXCEEDED`   | Every upstream provider tripped its cost ceiling                                                                                             |

For everything else, branch on the HTTP `status` and the `error` string.

## Error Handling Examples

### JavaScript/TypeScript

```javascript theme={null}
class GeroNexusError extends Error {
  constructor(status, body) {
    super(body.message || `HTTP ${status}`);
    this.name = 'GeroNexusError';
    this.status = status;
    this.errorCode = body.errorCode;       // may be undefined
    this.requestId = body.requestId;
    this.validationErrors = body.validationErrors;
  }
}

async function apiRequest(endpoint, options = {}) {
  const response = await fetch(`https://nexus.gerowallet.io${endpoint}`, {
    ...options,
    headers: {
      'X-Api-Key': process.env.NEXUS_API_KEY,
      ...options.headers
    }
  });

  const body = await response.json().catch(() => ({}));
  if (!response.ok) throw new GeroNexusError(response.status, body);
  return body;
}

try {
  const block = await apiRequest('/api/blocks/latest');
  console.log(block);
} catch (error) {
  if (error instanceof GeroNexusError) {
    switch (error.status) {
      case 401:
        console.error('Check your X-Api-Key header'); break;
      case 429:
        console.error('Rate limited — back off'); break;
      case 404:
        console.error('Not found:', error.message); break;
      default:
        console.error(`API ${error.status} (${error.requestId}):`, error.message);
    }
  } else {
    console.error('Network error:', error);
  }
}
```

### Python

```python theme={null}
import requests

class GeroNexusError(Exception):
    def __init__(self, status: int, body: dict):
        super().__init__(body.get('message', f'HTTP {status}'))
        self.status = status
        self.error_code = body.get('errorCode')       # may be None
        self.request_id = body.get('requestId')
        self.validation_errors = body.get('validationErrors')


def api_request(endpoint: str, method: str = 'GET', **kwargs) -> dict:
    response = requests.request(
        method,
        f'https://nexus.gerowallet.io{endpoint}',
        headers={'X-Api-Key': os.environ['NEXUS_API_KEY']},
        **kwargs,
    )
    body = response.json() if response.content else {}
    if not response.ok:
        raise GeroNexusError(response.status_code, body)
    return body


try:
    block = api_request('/api/blocks/latest')
    print(block)
except GeroNexusError as e:
    if e.status == 401:
        print('Check your X-Api-Key header')
    elif e.status == 429:
        print('Rate limited — back off')
    else:
        print(f'API {e.status} ({e.request_id}): {e}')
except requests.RequestException as e:
    print(f'Network error: {e}')
```

## Retry Strategy

Retry transient errors (5xx and the per-second 429) with exponential backoff. Do **not** loop on
a 4xx that won't change on retry (400, 401, 404), or on a monthly-quota 429 (see
[Rate Limits](/docs/concepts/rate-limits)).

```javascript theme={null}
async function fetchWithRetry(fn, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const retryable = error.status >= 500 || error.status === 429;
      if (!retryable || attempt === maxRetries) throw error;

      const delay = Math.min(1000 * 2 ** attempt, 30000);
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}
```

## Best Practices

<CardGroup cols={2}>
  <Card title="Branch on status / errorCode" icon="code">
    Use the HTTP `status` (and `errorCode` when present) for programmatic handling, not the
    human-readable `message`.
  </Card>

  <Card title="Log the requestId" icon="file-lines">
    Capture `requestId` from error responses so support can trace the exact failure.
  </Card>

  <Card title="Retry transient errors" icon="rotate">
    For 5xx and per-second 429s, retry with exponential backoff. Don't retry 4xx that won't change.
  </Card>

  <Card title="Always check status" icon="check">
    Never assume success — check the HTTP status before parsing the body.
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Rate Limits" icon="gauge" href="/docs/concepts/rate-limits">
    Understand rate limiting and quotas
  </Card>

  <Card title="Pagination" icon="list" href="/docs/concepts/pagination">
    Working with paginated responses
  </Card>

  <Card title="Authentication" icon="key" href="/docs/authentication">
    Learn about API authentication
  </Card>

  <Card title="API Reference" icon="book" href="/docs/api-reference">
    Explore all available endpoints
  </Card>
</CardGroup>
