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

# Errors

> The error taxonomy, and which failures are worth retrying.

Success and failure are carried by the HTTP status code. There is no envelope to unwrap — a `200`
body is the object itself, and any `4xx` or `5xx` body is an error object.

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "filters.revenue.gte must be a non-negative number.",
    "param": "filters.revenue.gte",
    "doc_url": "https://unif.dev/docs/platform/errors#parameter_invalid",
    "request_id": "req_01k3m9x7v2q8r4t6y0b1n5d7fa"
  }
}
```

<ResponseField name="type" type="string">
  The broad class. Branch on this — it is stable.
</ResponseField>

<ResponseField name="code" type="string">
  The specific reason. New codes can be added within an existing `type`, so treat an unrecognized
  code as its `type`.
</ResponseField>

<ResponseField name="param" type="string | null">
  The offending field as a dot-path, matching your request body exactly.
</ResponseField>

<ResponseField name="request_id" type="string">
  Also returned as the `X-Request-Id` header on **every** response. Log it on both success and
  failure — it is the only thing that lets support trace a specific call.
</ResponseField>

<Note>
  `message` is written for a human reading a log. It is not stable across releases — never parse
  it or match on its text. Branch on `type` and `code`.
</Note>

## Types

| Status | `type`                  | Retry?                                    |
| ------ | ----------------------- | ----------------------------------------- |
| 400    | `invalid_request_error` | No — fix the request                      |
| 401    | `authentication_error`  | No — fix the key                          |
| 403    | `permission_error`      | No                                        |
| 404    | `not_found_error`       | No                                        |
| 422    | `coverage_error`        | No — the field or market is not available |
| 429    | `rate_limit_error`      | Yes — after `Retry-After`                 |
| 402    | `billing_error`         | No — until credits are topped up          |
| 5xx    | `api_error`             | Yes — with backoff                        |

## Common codes

<AccordionGroup>
  <Accordion title="parameter_invalid" icon="circle-exclamation">
    A parameter has the wrong type or an impossible value. `param` names it.

    The usual cause is a rate passed as a percentage. `revenue_growth_rate` is a ratio — `0.5` is
    +50%. Passing `50` is a request for +5,000% growth, and it is accepted as valid, so it returns
    an empty list rather than an error.
  </Accordion>

  <Accordion title="parameter_missing" icon="circle-exclamation">
    A required parameter is absent. Most often `market` on a search, which cannot be inferred.
  </Accordion>

  <Accordion title="invalid_api_key" icon="key">
    The key is missing, malformed or revoked. Check the `Authorization` header is present and
    formatted `Bearer unif_sk_…`.
  </Accordion>

  <Accordion title="field_not_filterable" icon="filter">
    You filtered on a field that is not filterable for this channel and market. Unif rejects the
    query rather than returning a silently biased result set. Check
    [`GET /coverage`](/docs/concepts/coverage).
  </Accordion>

  <Accordion title="channel_not_enabled" icon="ban">
    The channel or market is not enabled for your workspace. An entitlement problem — see
    [`GET /channels`](/docs/concepts/channels).
  </Accordion>

  <Accordion title="period_out_of_range" icon="calendar">
    The requested period starts before the market's `earliest_period`, or ends in the future.
  </Accordion>

  <Accordion title="insufficient_credits" icon="coins">
    No credits left in this billing period. See [Credits](/docs/platform/credits).
  </Accordion>

  <Accordion title="idempotency_key_reused" icon="rotate">
    The same `Idempotency-Key` was sent with a **different** body. Keys are bound to the request
    that created them. See [Idempotency](/docs/platform/idempotency).
  </Accordion>
</AccordionGroup>

## Retrying well

Retry `429` and `5xx`. Never retry a `4xx` other than `429` — the request will fail identically
every time.

```python theme={null}
import random, time, requests

RETRYABLE = {429, 500, 502, 503, 504}

def call(method, url, **kwargs):
    for attempt in range(5):
        resp = requests.request(method, url, timeout=30, **kwargs)

        if resp.status_code not in RETRYABLE:
            return resp                                    # success, or a real failure

        if resp.status_code == 429:
            delay = float(resp.headers.get("Retry-After", 1))
        else:
            delay = (2 ** attempt) + random.uniform(0, 1)   # jitter matters

        time.sleep(delay)

    resp.raise_for_status()
```

Two details that are easy to skip and expensive to skip:

* **Honor `Retry-After` on a 429.** Backing off on your own schedule keeps you rate limited longer.
* **Add jitter.** Without it, every worker that hit the limit together retries together.

<Warning>
  Retrying a `POST /jobs` or `POST /enrich` without an `Idempotency-Key` can charge you twice for
  the same work. Send one on every non-idempotent call you might retry.
</Warning>

## When you need support

Include the `request_id`. It identifies the exact call, its parameters and what happened
internally, and it is the difference between a same-day answer and a long conversation.

```python theme={null}
resp = requests.post(url, headers=auth, json=body, timeout=30)
log.info("unif %s %s request_id=%s", resp.status_code, url, resp.headers.get("X-Request-Id"))
```
