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

# Errors & rate limits

> The error envelope, the codes you can branch on, and concurrency limits.

## The error envelope

Every non-`2xx` response has the **same shape** — a single `error` object:

```json theme={null}
{
  "error": {
    "code": "insufficient_credits",
    "message": "You're out of credits. Top up to run jobs.",
    "balance": 0
  }
}
```

<Warning>
  Branch on **`code`** — it is stable and machine-readable. Never match on `message`: it is
  human-facing text that may change without notice.
</Warning>

Some errors add **context** fields: `balance` on credit errors, `limit` on concurrency errors.

## Codes

| Status | `code`                     | Meaning                                                    | Fix                                                                     |
| ------ | -------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------- |
| `400`  | `invalid_input`            | The `input` couldn't be parsed into a company.             | Send a LinkedIn company URL, slug, or numeric id.                       |
| `400`  | `unsupported_operation`    | Unknown `operation`.                                       | Use `target_people`.                                                    |
| `400`  | `persona_required`         | No persona slug was submitted.                             | Pass an existing persona slug.                                          |
| `400`  | `persona_not_found`        | The submitted persona slug is not in your account.         | List or create it with `/v1/personas`, then retry.                      |
| `400`  | `invalid_idempotency_key`  | `Idempotency-Key` is empty or over 128 characters.         | Send a stable 1–128 character key or omit the header.                   |
| `409`  | `idempotency_conflict`     | The key was already used for a different request.          | Reuse the original payload or submit with a new key.                    |
| `409`  | `system_persona_immutable` | An edit/delete targeted Owners & founders.                 | Choose a custom persona instead.                                        |
| `401`  | `missing_api_key`          | No `X-API-Key` header.                                     | Add your key.                                                           |
| `401`  | `invalid_api_key`          | Key is unknown or inactive.                                | Check the key in the [dashboard](https://app.firmintent.com/dashboard). |
| `402`  | `insufficient_credits`     | Balance is not positive (carries `balance`).               | [Top up](/credits).                                                     |
| `404`  | `job_not_found`            | No such job under your key.                                | Check the `job_id`; jobs are scoped to the key that created them.       |
| `422`  | `validation_error`         | Request body/params failed validation (carries `details`). | Fix the fields listed in `details`.                                     |
| `429`  | `concurrency_limit`        | Too many jobs in flight (carries `limit`).                 | Wait for one to finish, or upgrade.                                     |

## Handling errors

```python Python theme={null}
r = requests.post(URL, headers=HEADERS, json=payload)
if not r.ok:
    err = r.json()["error"]
    if err["code"] == "insufficient_credits":
        top_up()                         # balance in err["balance"]
    elif err["code"] == "concurrency_limit":
        wait_and_retry()                 # plan limit in err["limit"]
    else:
        raise RuntimeError(err["message"])
```

## Concurrency and rate limits

There is no per-second request limit on reads. The one throttle is **job concurrency**: how many
jobs may be *in flight* (submitted but not yet done) at once. It's set by your plan — free plans run
**one at a time**.

Over the limit, `POST /v1/jobs` returns:

```json theme={null}
{ "error": { "code": "concurrency_limit",
             "message": "Too many jobs in flight for your plan (limit 1). Wait for a job to finish or upgrade your plan.",
             "limit": 1 } }
```

Your live limit is the `concurrency_limit` field in [`GET /v1/account`](/credits#checking-your-balance).
Poll your in-flight jobs to completion, then submit the next — or move to a plan with a higher limit.

<Note>
  Re-polling a **finished** job is always free and never counts against your limit, so a lost
  response never needs a new job.
</Note>

## Terminal job states

Some outcomes are successful HTTP responses with a terminal job `status`, not error envelopes.
In particular, a non-premium search above the company-size gate returns `200` with
`{ "status": "too_large", "message": "…" }`; it starts no pipeline work and costs nothing.
`not_found`, `no_employees`, `no_people_found`, `too_large`, `failed`, and `cancelled` are terminal
statuses to handle from the poll result. They carry numeric `credits_charged: 0`.
