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

# Errors & Limits

> Every Topo API error follows a consistent JSON envelope. Learn the error taxonomy, how to handle rate limits, and how to use request IDs for support.

When something goes wrong, the Topo API always returns a structured JSON error body alongside an appropriate HTTP status code. The error shape is the same regardless of the endpoint or error type, so you can build a single error-handling layer for your entire integration.

## Error response shape

Every error response body contains the following fields:

<ResponseField name="status_code" type="integer" required>
  The HTTP status code of the response (e.g. `404`, `422`, `429`). Mirrors the HTTP status on the response itself.
</ResponseField>

<ResponseField name="type" type="string" required>
  A stable string identifier for the error category (e.g. `NotFoundIssue`). See the taxonomy below. This value is **frozen** — it will not be renamed in `/v1`.
</ResponseField>

<ResponseField name="message" type="string">
  A human-readable description of what went wrong. Useful for logging and debugging; do not rely on the exact wording in application logic.
</ResponseField>

<ResponseField name="data" type="object">
  Optional structured detail about the error — for example, which field failed validation or what the applicable rate limit is.

  <Expandable title="Example data fields">
    Validation errors may include the field name and the rule that failed. Rate limit errors include the `limit`, `reset_at` Unix timestamp, and `window_seconds` of the exceeded bucket.
  </Expandable>
</ResponseField>

<ResponseField name="request_id" type="string">
  A unique identifier for this specific request, generated by Topo's API gateway. Always include this value when contacting support — it's the fastest way to locate your request in our logs.
</ResponseField>

<ResponseField name="trace_id" type="string">
  Internal trace identifier. Present in non-production environments for debugging.
</ResponseField>

<ResponseField name="span_id" type="string">
  Internal span identifier. Present in non-production environments for debugging.
</ResponseField>

**Example error response**

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status_code": 404,
  "type": "NotFoundIssue",
  "message": "Contact not found",
  "request_id": "req_01j9kx3m7p0000000000000000"
}
```

## Error taxonomy

The `type` field uses a frozen set of values. Branch first on `status_code`, then use `type` as a stable refinement for more specific handling.

<Note>
  The `type` identifiers listed below are **frozen** and will not be renamed in `/v1`. Adding a new error type is an additive change; renaming an existing one is breaking and would require `/v2`.
</Note>

| `type`                | HTTP status | When it occurs                                                                                                                    |
| --------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `ValidationIssue`     | `400`       | The request body or query parameters failed validation — a required field is missing, a value is the wrong type, etc.             |
| `UnauthorizedIssue`   | `401`       | The `Authorization` header is missing, malformed, or contains an invalid key                                                      |
| `UnauthorizedIssue`   | `403`       | The API key is valid but lacks the required scope for this endpoint                                                               |
| `NotFoundIssue`       | `404`       | The requested resource does not exist or does not belong to your workspace                                                        |
| `CommitIssue`         | `409`       | The operation conflicted with the current state of the resource — for example, a concurrent modification or uniqueness constraint |
| `BusinessIssue`       | `422`       | The operation is not allowed given the current state — for example, enrolling a contact who is already excluded                   |
| `RateLimitIssue`      | `429`       | Your organization or API key has exceeded a rate limit                                                                            |
| `InternalServerIssue` | `500`       | An unexpected error occurred on Topo's side                                                                                       |

### ValidationIssue (400)

Returned when the request itself is malformed. `data.param` points at the first offending field, and `data.errors` lists every validation failure with its `field`, `message`, and `type`.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status_code": 400,
  "type": "ValidationIssue",
  "message": "value is not a valid email address",
  "data": {
    "param": "body -> email",
    "errors": [
      {
        "field": "body -> email",
        "message": "value is not a valid email address",
        "type": "value_error"
      }
    ]
  },
  "request_id": "req_01j9kx3m7p0000000000000000"
}
```

### UnauthorizedIssue (401 / 403)

A `401` means no valid key was provided; a `403` means the key is valid but does not have the required scope. Check the `data.required` field to see which scope is needed.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status_code": 403,
  "type": "UnauthorizedIssue",
  "message": "API key lacks required scope",
  "data": {
    "required": "contacts:write"
  },
  "request_id": "req_01j9kx3m7p0000000000000000"
}
```

### NotFoundIssue (404)

Returned when a resource ID does not exist or belongs to a different workspace. Topo deliberately returns 404 (rather than 403) for cross-workspace resources to avoid leaking the existence of records.

### RateLimitIssue (429)

Returned when your organization exceeds the allowed request rate. See the [rate limits section](#rate-limits) below for how to handle this response.

## Rate limits

Topo enforces rate limits per organization across three time windows simultaneously: per second, per minute, and per day. All three windows are active on every request.

Every API response includes headers that tell you your current rate limit state:

| Header                         | Description                                                       |
| ------------------------------ | ----------------------------------------------------------------- |
| `X-RateLimit-Limit-second`     | Maximum requests allowed per second                               |
| `X-RateLimit-Remaining-second` | Requests remaining in the current second                          |
| `X-RateLimit-Reset-second`     | Unix timestamp when the second window resets                      |
| `X-RateLimit-Limit-minute`     | Maximum requests allowed per minute                               |
| `X-RateLimit-Remaining-minute` | Requests remaining in the current minute                          |
| `X-RateLimit-Reset-minute`     | Unix timestamp when the minute window resets                      |
| `X-RateLimit-Limit-day`        | Maximum requests allowed per day                                  |
| `X-RateLimit-Remaining-day`    | Requests remaining in the current day                             |
| `X-RateLimit-Reset-day`        | Unix timestamp when the day window resets                         |
| `Retry-After`                  | Seconds to wait before retrying (present only on `429` responses) |

When a rate limit is exceeded, the API returns `429` with a `RateLimitIssue` error body:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status_code": 429,
  "type": "RateLimitIssue",
  "message": "Rate limit exceeded",
  "data": {
    "limit": 60,
    "reset_at": 1737043200,
    "window_seconds": 60
  },
  "request_id": "req_01j9kx3m7p0000000000000000"
}
```

### Handling rate limits

<Steps>
  <Step title="Read the Retry-After header">
    On a `429` response, read the `Retry-After` header value (in seconds) and wait at least that long before retrying.
  </Step>

  <Step title="Apply exponential backoff">
    If you continue to receive `429` responses, increase your wait time exponentially with jitter to avoid synchronized retries across parallel workers.
  </Step>

  <Step title="Monitor remaining capacity proactively">
    Watch `X-RateLimit-Remaining-minute` on successful responses. If it drops near zero, slow down your request rate before hitting the limit.
  </Step>
</Steps>

<Warning>
  Repeatedly sending requests after receiving a `429` will not speed up the reset — it will only consume capacity from subsequent windows. Always respect the `Retry-After` value.
</Warning>

Additionally, repeated authentication failures (e.g. using an invalid key in a loop) will trigger a temporary block on that key's token hash. Ensure your key is correct before automating requests.

## Debugging tips

<Tip>
  Always log the `request_id` from every API response — successful or failed. When you contact Topo support, providing the `request_id` lets the team locate your exact request instantly.
</Tip>

* **Branch on `status_code` first** for broad categories (auth, not found, server error), then use `type` to distinguish sub-cases like `401` vs `403`.
* **Don't match on `message`** — the human-readable message text is for display only and may change over time. Use `type` for logic.
* **Log `data` for validation errors** — it contains field-level detail that helps you identify what to fix in your request.
* **5xx errors are Topo-side** — if you receive a `500 InternalServerIssue`, the issue is not with your request. Retry with backoff and contact support if it persists, providing the `request_id`.
