# Errors (/docs/errors)



LinkFetch uses standard HTTP status codes. Every error response has the
same JSON shape:

```json
{
  "error": "not_found",
  "message": "No profile found for url=linkedin.com/in/does-not-exist.",
  "request_id": "req_2Yq7zR1tDkH"
}
```

`error` is a stable machine code; `message` is a human-readable
explainer that may change wording. &#x2A;*Branch on `error`, log
`message`.** `request_id` matches the `X-Request-Id` response header
and our internal logs — quote it when you open a ticket and we can
look up the trace in seconds.

## Status codes [#status-codes]

| Status                                  | When                                                                                              |
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `400 bad_request`                       | Malformed query, missing required param, unparseable URL.                                         |
| `401 missing_key` / `invalid_key`       | No `Authorization` header, or the key is wrong / revoked.                                         |
| `402 insufficient_credits`              | Workspace balance is zero — top up on the [billing page](/billing).                               |
| `403 scope_denied`                      | Key is scoped and the endpoint is out of scope.                                                   |
| `404 not_found`                         | The record doesn't exist (and has never existed in our cache).                                    |
| `410 gone`                              | The record was suppressed under a DSR — see [Compliance](/docs/compliance).                       |
| `422 extension_required`                | Cache miss on an extension-backed read; the LinkFetch Chrome extension needs to capture this row. |
| `429 rate_limit` / `outbound_throttled` | See [Rate limits](/docs/rate-limits).                                                             |
| `5xx internal_error`                    | Our problem — `request_id` already paged us.                                                      |

## The `422 extension_required` flow [#the-422-extension_required-flow]

Profile, company, post, group, and search reads are **cache-first**.
On a cold miss the API doesn't synthesize the row from a fake account —
it returns a 422 telling the LinkFetch Chrome extension to capture the
page from the user's own signed-in LinkedIn tab and POST it back to
`/ingest`. From that point on, the cache hits like any other read.

```json
{
  "error": "extension_required",
  "message": "No cached row for url=linkedin.com/in/ada-lovelace. The LinkFetch extension on a signed-in tab can capture it.",
  "request_id": "req_2Yq7zR1tDkH",
  "capture_hint": {
    "url": "https://www.linkedin.com/in/ada-lovelace/",
    "extension_message": "linkfetch:extension:fetch-profile"
  }
}
```

The [extension](/docs/extension) handles the round-trip automatically
when you run the call from the [playground](/playground) — you don't
have to wire the bridge yourself unless you're embedding the flow in
your own app.

## Idempotency [#idempotency]

POST endpoints accept an `Idempotency-Key` header (any opaque string,
typically a UUID). LinkFetch caches the **response** for 24 hours
keyed on `(api_key, idempotency_key)` — replays return the original
response, including the original `request_id`, with status `200` and
header `X-Idempotent-Replay: true`.

```bash
curl -X POST https://api.linkfetch.io/v1/outbound/connections \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: 0b8e2a76-..." \
  -H "Content-Type: application/json" \
  --data '{"slug":"reidhoffman"}'
```

## Validation errors [#validation-errors]

`400 bad_request` errors include a `details` array with the offending
fields:

```json
{
  "error": "bad_request",
  "message": "Validation failed: 1 error.",
  "request_id": "req_2Yq7zR1tDkH",
  "details": [
    { "field": "limit", "code": "out_of_range", "expected": "1..50", "got": "200" }
  ]
}
```

## Retries [#retries]

* **5xx**: exponential backoff with jitter, up to 5 attempts.
* **429**: honour `Retry-After`, then jittered fixed delay.
* **422 extension\_required**: the extension flow is async — surface a
  "Capture via extension" prompt rather than retrying.
* **Everything else**: do not retry; the request is deterministic.

## When in doubt [#when-in-doubt]

Quote the `request_id`. We can pull the full trace, the params, the
response, and the credit ledger entry from one identifier.
