# Error handling & retries

> Handle API errors predictably with safe retry strategies.

Every error response follows the same structure and includes a clear error code you can match on programmatically.

## Response format

Every error follows the same JSON structure. Match on the `code` field to handle specific cases in your code:

**Error Response**

```json [JSON]
{
  "success": false,
  "data": null,
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Insufficient credits: 0 available, 1 required"
  },
  "metadata": {
    "requestId": "00bc3fa0-348f-4955-a1c1-3c7c91234568",
    "executionTimeMs": 3
  }
}
```

> [!NOTE]
> Always include `metadata.requestId` when contacting support. It lets us trace your request instantly.

## Error reference

| Status | What to do |
| ------ | ---------- |
| `422` | Check your request body: a required field is missing or malformed (`INVALID_LINKEDIN_URL` is also returned for an invalid URL). For async endpoints, make sure a webhook URL is configured. On the search endpoints, see [Search filter validation](#search-filter-validation) for the exact reason. |
| `401` | Check your API key: it may be missing, malformed, invalid, or expired. See [Authentication](https://docs.reversecontact.com/authentication). |
| `402` | No credits left. Paid lookups (`INSUFFICIENT_CREDITS`) and the free `/check` endpoints (`NO_CREDITS`) are both blocked until you top up. Purchase more or [upgrade your plan](https://app.reversecontact.com/settings/billing). |
| `403` | Your workspace may be disabled (`WORKSPACE_DISABLED`), your key may lack a required permission, or the endpoint may require a paid plan. [Upgrade](https://app.reversecontact.com/settings/billing?tab=credits) to unlock live enrichment, email discovery, and more. |
| `404` | No result found for your query. Some endpoints still charge credits; see [Not found pricing](https://docs.reversecontact.com/guides/rate-limits-credits#note). |
| `429` | Rate limit reached. Wait and retry with backoff (see below). |
| `451` | This person has been removed per GDPR/CCPA. Enrichment is permanently blocked. |
| `503` | Our service is temporarily unavailable. Retry with backoff (see below). |
| `504` | The lookup took too long and timed out. Retry with backoff (see below). |

## Search filter validation

The search endpoints validate every filter before running the query. A rejected request returns `422`, **costs 0 credits**, and never reaches the database. This is deliberate: a filter we cannot honour would otherwise return an empty page that looks like a legitimate "no results", and bill you for it.

`error.details.errors` always lists every problem found, so one round trip is enough to fix a whole payload.

| `error.code` | Meaning |
| ------------ | ------- |
| `UNKNOWN_FIELD` | The field does not exist. Unknown fields are rejected rather than ignored, because an ignored filter returns unfiltered results. `details.suggestion` gives the closest accepted field, `details.acceptedFields` lists them all. |
| `INVALID_FILTER_VALUE` | The value is outside a closed list. This covers industry and country values plus the People Search `currentFunction`, `currentSubFunction` and `currentSeniority` taxonomies and the `changedJobsWithinDays` windows, as well as `keywords` without a single word. `details.suggestions` proposes up to three valid values. |
| `EMPTY_FILTER_VALUE` | A blank string, empty array or empty object was sent. Remove the field instead. |
| `FILTER_VALUES_LIMIT_EXCEEDED` | A filter carries more than 20 values. Split it into searches of at most 20 values and merge the results. |
| `INVALID_RANGE` | A `min`/`max` pair is negative, fractional, or inverted (`min` greater than `max`). |
| `INCOMPLETE_RANGE_FILTER` | `followersCount` used as the only filter requires both bounds. |
| `INVALID_PAGINATION` | `perPage`, or `page` on Company Search, is not a whole number. |
| `PAGINATION_LIMIT_EXCEEDED` | `perPage` is above 100, or `page` is above 50 on Company Search. `details` carries `field` and `max`. |
| `INVALID_ID_FORMAT` | `currentCompanyId` is not a `com_` identifier returned by our API. |
| `INVALID_DATE_FORMAT` | `maxDataAgeDate` is not an ISO 8601 date-time. |
| `CONTRADICTORY_FILTERS` | The same value appears in an included and excluded filter, which can never match. |
| `NO_FILTER_PROVIDED` | The body carries pagination only: `cursor` / `perPage` for People Search or `page` / `perPage` for Company Search. |
| `INVALID_SEARCH_FILTERS` | Several of the above at once. Read `details.errors`. |

```json [Response]
{
  "success": false,
  "data": null,
  "error": {
    "code": "INVALID_FILTER_VALUE",
    "message": "\"Technology\" is not an accepted value for \"industry\". Did you mean \"Technology, Information and Internet\", \"Technology, Information and Media\"? This filter is matched exactly, so an unlisted value can never return a result.",
    "details": {
      "field": "industry",
      "value": "Technology",
      "reason": "not_in_accepted_values",
      "suggestions": ["Technology, Information and Internet", "Technology, Information and Media"],
      "documentation": "https://docs.reversecontact.com/guides/industries"
    }
  }
}
```

Values are compared tolerantly before being rejected: case, surrounding spaces, accents and `&` versus `and` are normalised for you. Only a genuinely unknown value is refused.

## When to retry

These statuses are safe to retry with exponential backoff:

- **`429`**
  you've hit a rate limit. Wait for the reset window and retry.
- **`503`**
  temporary service issue. Retry after a short delay.
- **`504`**
  the lookup timed out before completing. Retry after a short delay.

All other errors require a change to your request or configuration before retrying.

Wait a little longer between each retry to give the server time to recover:

**Retry delay**

```javascript [JavaScript]
const delay = Math.min(1000 * 2 ** attempt, 15000)
const jitter = Math.floor(Math.random() * 250)
await new Promise((r) => setTimeout(r, delay + jitter))
```
```python [Python]
delay = min(1 * 2 ** attempt, 15)
jitter = random.uniform(0, 0.25)
time.sleep(delay + jitter)
```

## Credits and errors

All error responses (`401`, `402`, `403`, `422`, `429`, `451`, `503`, `504`) cost 0 credits.

The `404` status is the only exception: some endpoints charge credits when the lookup was performed but no result was found. See [Not found pricing](https://docs.reversecontact.com/guides/rate-limits-credits#note) for the full breakdown.
