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

# Errors and retries

> Retry safely with idempotency keys, handle rate limits, and recover from every kind of failure.

Starting a search costs money. So when a request fails, the first thing to find out is whether the search started anyway. This guide shows how to retry safely and what to do for each error.

## Make starts safe with an idempotency key

An operation is idempotent when doing it twice has the same effect as doing it once. Starting a search is not idempotent on its own: two `POST` requests start two searches. An idempotency key fixes that. It is a unique string you choose and send in the `Idempotency-Key` header, and it tells the API "this is the same request as before".

Send one on every request that starts a search. Any unique string of 1 to 255 characters works. A UUID is a good choice.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.avina.io/v1/companies/search \
  -H "Authorization: Bearer $AVINA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f0c1b9e-2a47-4f6d-b1a0-93c5d7e8f012" \
  -d '{ "query": "Series B fintech companies in New York", "limit": 20 }'
```

What the key does:

| You send | You get |
| - | - |
| A new key | The request runs. |
| The same key and the same body again, within 24 hours | The original response, with the header `Idempotent-Replayed: true`. No second search is started or charged. |
| The same key with a different body | `409 idempotency_key_reused`. Use a new key for a new request. |
| The same key while the first request is still being handled | `409 idempotency_key_reused`. Wait a few seconds and retry. |

> [!WARNING]
>
> Without an idempotency key, every `POST` starts a new search. If a request times out and you retry it, you can end up paying for two searches.

### Generate the key before the first attempt

The key identifies what you are trying to do, not one HTTP request. Create it once, store it alongside whatever triggered the search, and reuse it on every retry of that search.

```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import os, time, uuid, requests

API = "https://api.avina.io/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['AVINA_API_KEY']}"}
RETRYABLE = {429, 500, 502, 503, 504}

def start_search(body: dict, kind: str = "companies", attempts: int = 5) -> dict:
    key = str(uuid.uuid4())  # one key for every attempt
    for attempt in range(attempts):
        try:
            response = requests.post(
                f"{API}/{kind}/search",
                headers={**HEADERS, "Idempotency-Key": key},
                json=body,
                timeout=30,
            )
        except requests.RequestException:
            response = None  # network failure: we do not know if it started

        if response is not None and response.status_code not in RETRYABLE:
            if response.status_code == 409:
                time.sleep(2)  # first attempt still in progress
                continue
            response.raise_for_status()
            return response.json()

        retry_after = response.headers.get("Retry-After") if response is not None else None
        time.sleep(int(retry_after) if retry_after else min(2 ** attempt, 30))
    raise RuntimeError("Could not start the search")
```

```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const API = "https://api.avina.io/v1";
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function startSearch(body, kind = "companies", attempts = 5) {
  const key = crypto.randomUUID(); // one key for every attempt
  for (let attempt = 0; attempt < attempts; attempt++) {
    let response = null;
    try {
      response = await fetch(`${API}/${kind}/search`, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.AVINA_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": key,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(30000),
      });
    } catch {
      // network failure: we do not know if it started
    }

    if (response && !RETRYABLE.has(response.status)) {
      if (response.status === 409) { await sleep(2000); continue; }
      if (!response.ok) throw new Error(JSON.stringify(await response.json()));
      return response.json();
    }

    const retryAfter = response?.headers.get("Retry-After");
    await sleep(retryAfter ? Number(retryAfter) * 1000 : Math.min(2 ** attempt, 30) * 1000);
  }
  throw new Error("Could not start the search");
}
```

## What to retry

| Status | `code` | Retry? | How |
| - | - | - | - |
| `400`, `413`, `415` | `invalid_request` | No | Fix the request. `details` lists every problem. |
| `401` | `invalid_api_key` | No | Check the key and the `Authorization` header. |
| `402` | `usage_limit_reached` | No | Add credits. Nothing was started or charged. |
| `403` | `permission_denied` | No | The workspace is not allowed to do this. |
| `404` | `not_found` | No | Check the ID. It must belong to the key's workspace. |
| `409` | `idempotency_key_reused` | Sometimes | If the first request is still in progress, retry shortly. If the body differs, use a new key. |
| `429` | `rate_limited` | Yes | Wait `Retry-After` seconds, then retry. |
| `500`, `502` | `internal_error` | Yes | Retry with backoff (Waiting longer after each failed attempt, for example 1, 2, 4 and then 8 seconds, so you do not overload a service that is struggling.) and the same idempotency key. |
| `503` | `internal_error`, `workspace_not_ready` | Yes | Retry with backoff. |
| `504` | `timeout` | Yes | Retry with the same idempotency key. |

`GET` requests are always safe to retry. They never start work or spend credits.

## When you do not know if a search started

A connection can drop after the API accepted your request but before you saw the response. The search may be running and spending credits.

**1. Retry with the same idempotency key**

If the first request finished, you get its response back, including the search `id`. If it never arrived, the search starts now.

**2. If you get a 409, wait and retry**

A `409` with the same body means the first request is still being handled. Retry every few seconds.

**3. If the 409 lasts more than five minutes, stop**

The first attempt may have ended without a recorded response. Do not keep retrying. Check `GET /usage` for spend from the first attempt, and contact support with the `request_id` if you see spend you cannot account for.

**4. If you never sent a key**

The API cannot tell you whether the search started. Check `GET /usage` for unexpected spend, and send a key next time.

## Rate limits

| Limit | Value | When exceeded |
| - | - | - |
| Requests per API key | 120 per minute | `429 rate_limited` |
| Searches running at the same time | No limit | |

Every response includes your current standing:

| Header | Meaning |
| - | - |
| `RateLimit-Limit` | Requests allowed per window |
| `RateLimit-Remaining` | Requests left in the current window |
| `RateLimit-Reset` | Seconds until the window resets |
| `Retry-After` | On a `429`, seconds to wait before retrying |

To stay under the limit, leave longer gaps between polls as shown in [Poll and paginate](https://docs.data.avina.io/guides/poll-and-paginate.md), and read results in pages of 100.

## Errors that are not HTTP errors

Some failures happen after a search was accepted. They show up on the search, not as an error response.

| What you see | Meaning | What to do |
| - | - | - |
| `status: "failed"` | The search could not finish. | Read `error`. Read the results found before the failure. Start a new search, with a new idempotency key, if you need more. |
| `stop_reason: "query_needs_clarification"` | The query could not be searched as written. Nothing was charged. | Read `interpretation.unsupported_criteria` and `interpretation.errors`, then reword. |
| `stop_reason: "usage_limit_reached"` | The search hit `max_credits` or the workspace ran out. | Results so far are kept. Add credits or raise the cap. |
| `contact_status: "partial"` or `"failed"` | Some contact lookups did not finish. | The company is still valid. Search again if you need the contacts. |

## Getting help

Every response has an `X-Request-Id` header, and every error body repeats it as `request_id`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "error": {
    "code": "internal_error",
    "message": "Something went wrong on our side. Retry the request; if it keeps failing, contact support with the request id.",
    "request_id": "req_8f2a61c09d4e7b3a5f10c2d8"
  }
}
```

When you contact [support@avina.io](mailto:support@avina.io), include the `request_id` and, for a search, its `id`. Never send your API key.

> [!NOTE]
>
> Log `request_id` for failed requests. Do not log the `Authorization` header.

## Related

* [Errors reference](https://docs.data.avina.io/api-reference/errors.md): every code
* [Search lifecycle](https://docs.data.avina.io/concepts/search-lifecycle.md): statuses and stop reasons


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.