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

# Errors reference

> Every status the API returns, the error code that comes with it, and what to change before retrying.

Every failure the endpoints build themselves carries a `message` written for a person and a short `error` string your code can branch on instead of parsing that message. Two statuses are the exception: a `422` answers with a per-field `errors` object and no `error` string, and a `429` comes from the rate limiter that sits in front of them.

<div className="pl-availability">
  <div className="pl-availability__row">
    <div className="pl-availability__label">Where</div>
    <div className="pl-availability__value"><span className="pl-path">Settings<span className="pl-path__sep">→</span>API & Integrations</span></div>
  </div>

  <div className="pl-availability__row">
    <div className="pl-availability__label">Your role needs</div>
    <div className="pl-availability__value">Update access to Lead intake API <code>update-companies</code>. Admin have it by default.</div>
  </div>

  <div className="pl-availability__note">Authenticated by a workspace API key, not by a signed-in session. Creating a key needs the same permission as the rest of the API settings tab.</div>
</div>

## Status and error codes

| Status                     | `error`                                          | Where                              | What to change                                                                                 |
| -------------------------- | ------------------------------------------------ | ---------------------------------- | ---------------------------------------------------------------------------------------------- |
| `400 Bad Request`          | `invalid_idempotency_key`                        | `POST /leads`, `POST /leads/batch` | Your `Idempotency-Key` header is longer than the API accepts: shorten it                       |
| `401 Unauthorized`         | `invalid_api_key`                                | Every request                      | The key is missing, malformed, revoked, expired, or orphaned: read the `message` to tell which |
| `403 Forbidden`            | `insufficient_scope`                             | Every request except `GET /me`     | The key doesn't carry the scope this route needs                                               |
| `404 Not Found`            | `not_found`                                      | `GET /leads/{id}`                  | No lead with that id in this workspace                                                         |
| `409 Conflict`             | `duplicate_lead`                                 | `POST /leads` only                 | `on_duplicate: "error"` and a matching lead already exists                                     |
| `409 Conflict`             | `idempotency_key_reuse`                          | `POST /leads`, `POST /leads/batch` | The same `Idempotency-Key` was sent with a different body                                      |
| `422 Unprocessable Entity` | none: read `errors` instead                      | `POST /leads`, `POST /leads/batch` | The payload fails validation                                                                   |
| `429 Too Many Requests`    | none: the rate limiter answers, not the endpoint | Every request                      | You're calling too often: slow down, or batch                                                  |

<Note>
  Two rows above share one status. A `409` from a duplicate lead and a `409` from a reused idempotency key are different problems with different fixes. Always read `error`, never branch on the status code alone.
</Note>

The order is fixed, and it decides which error you see when a call gets more than one thing wrong. The key is authenticated, then its scope is checked, then the payload is validated, and only then is the `Idempotency-Key` header looked at. So a call that has both a body that fails validation and an over-long key comes back as the `422`, never the `400`.

## 401 Unauthorized: a bad credential

Every problem with the credential itself comes back as a `401` with `error: "invalid_api_key"`, whatever caused it:

```json 401 Unauthorized theme={"system"}
{
  "message": "Invalid, revoked or expired API key.",
  "error": "invalid_api_key",
  "documentation_url": "https://help.pipelime.ai/en/developers/authentication"
}
```

One cause gets its own `message`, but the same `error` code: a key whose creating teammate no longer has an account at all is orphaned, and fails like this instead:

```json 401 Unauthorized: orphaned key theme={"system"}
{
  "message": "This API key is orphaned because the user who created it no longer exists. Create a new key.",
  "error": "invalid_api_key",
  "documentation_url": "https://help.pipelime.ai/en/developers/authentication"
}
```

Because the `error` field is identical for both, an integration that branches on it alone can't tell these apart. Only the `message` text does. Either way the fix is the same: create a new key. `documentation_url` points at the [authentication reference](/en/developers/authentication), which is where the two cases are explained. It is not a link into the workspace.

## 403 Forbidden: missing scope

A key that authenticates but wasn't granted the scope a route needs gets a `403`, naming exactly what's missing:

```json 403 Forbidden theme={"system"}
{
  "message": "This API key is missing the required scope [leads:write].",
  "error": "insufficient_scope",
  "required_scope": "leads:write",
  "granted_scopes": ["tags:read"]
}
```

`granted_scopes` lists what the key actually carries, so you can see the gap without a second request. `GET /me` is the one route with no scope requirement at all. It never returns this error.

## 404 Not Found: no such lead

`GET /leads/{id}` returns this when the id doesn't belong to a lead in your workspace, whether it never existed or belongs to someone else's:

```json 404 Not Found theme={"system"}
{
  "message": "Lead not found in this workspace.",
  "error": "not_found"
}
```

The route only matches a numeric id, so `GET /leads/abc` never reaches this check. It fails as an unmatched route instead. A `404` whose body has no `"error": "not_found"` in it is that, not a missing lead.

## 409 Conflict: two different causes

A duplicate lead only happens when you explicitly asked for it. `on_duplicate: "error"` on `POST /leads` rejects a matching lead instead of updating or skipping it:

```json 409 Conflict: duplicate lead theme={"system"}
{
  "message": "A lead with this email address already exists in this workspace.",
  "error": "duplicate_lead",
  "matched_on": "email address",
  "lead_id": 4821
}
```

`lead_id` is the existing lead, so you can fetch or update it directly instead of retrying the create. See [POST /leads](/en/developers/post-leads) for what `matched_on` can read and how a match is found.

The other `409` has nothing to do with lead data. It means you reused an `Idempotency-Key` with a request body that doesn't match the one it was first used with:

```json 409 Conflict: idempotency key reused theme={"system"}
{
  "message": "This Idempotency-Key was already used with a different request body.",
  "error": "idempotency_key_reuse"
}
```

The fix is a new key value, not a retry. Reusing the same key with the same body instead replays the original response, which is what the header is for. See [Idempotency, deduplication and rate limits](/en/developers/idempotency-deduplication-and-rate-limits) for the full mechanism.

## 422 Unprocessable Entity: no error code, read `errors`

Unlike every status above, a `422` carries no `error` field at all. What you get instead is a standard `errors` object, keyed by the field that failed:

```json 422: missing contact detail theme={"system"}
{
  "errors": {
    "email": [
      "At least one contact detail is required: email, phone or linkedin_url."
    ]
  }
}
```

That example shows the requirement every payload goes through: a `name`, plus at least one way to reach the person. The message names three fields, but a non-empty `emails` or `phones` array satisfies it too. If you sent one of those and still got this error, look elsewhere in the payload, not there. The failure is filed under `email` whichever of those five fields you meant to send, so read the message rather than the key.

On `POST /leads`, a `422` also covers everything else the request can fail on: an unknown tag name, a `company.id` from another workspace, a field over its length limit. Each lands under its own key in `errors`. [POST /leads](/en/developers/post-leads) lists the full set. In a batch, those same problems land inside a `200` as a failed row instead.

## 429 Too Many Requests

The whole `/api/v1` route group sits behind a rate limiter, and it answers before the endpoint does. So a `429` carries none of the `error` codes above, and the status is what you branch on. What the limit is, and what it's counted against, is on [Idempotency, deduplication and rate limits](/en/developers/idempotency-deduplication-and-rate-limits).

## Batch requests capture failures per row: with one exception

`POST /leads/batch` is built so one bad row can't throw away every other row in the call: once the call as a whole is accepted, it always returns `200`, and a row that couldn't be processed (an unknown tag, a duplicate under `on_duplicate: "error"`, an unexpected error) shows up inside the body instead of as an HTTP failure:

```json One row inside a 200 batch response theme={"system"}
{
  "index": 3,
  "status": "failed",
  "lead_id": 4821,
  "error": "A lead with this email address already exists in this workspace.",
  "field": "email"
}
```

A row's `error` holds the message, not one of the codes from the table above: those codes are top-level fields, never row fields.

<Warning>
  `field` on a duplicate row always reads `"email"`, even when the match was on a LinkedIn URL or a phone number. Read the message to see what actually matched. The single-lead `409` reports it properly, in `matched_on`.
</Warning>

That per-row capture only applies to problems found while a row is being processed, not to the shape of the row itself. `name`, contact details and every other field are validated against the whole array before any row runs, so one row missing a name or a contact detail fails the request the same way a single `POST /leads` would: a top-level `422`, and none of the batch's rows are processed. The key names the row that broke it (`leads.3.email`), so fix the payload and resend the whole batch.

A row that fails for an unanticipated reason gets a generic message instead of the real cause:

```json An unhandled row failure theme={"system"}
{
  "index": 7,
  "status": "failed",
  "lead_id": null,
  "error": "This lead could not be processed. Please retry or contact support.",
  "field": null
}
```

## Related

<CardGroup cols={2}>
  <Card title="POST /leads" icon="send" href="/en/developers/post-leads">
    Request body, duplicate handling and the full response envelope.
  </Card>

  <Card title="Create, roll and revoke API keys" icon="key" href="/en/developers/api-keys">
    Create a key, copy its secret once, and revoke it when you need to.
  </Card>

  <Card title="Idempotency, deduplication and rate limits" icon="repeat" href="/en/developers/idempotency-deduplication-and-rate-limits">
    Retry safely, and see what happens when you send too fast.
  </Card>
</CardGroup>
