Errors

One envelope, predictable codes, an auditable request id.

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

{
  "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. 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

StatusWhen
400 bad_requestMalformed query, missing required param, unparseable URL.
401 missing_key / invalid_keyNo Authorization header, or the key is wrong / revoked.
402 insufficient_creditsWorkspace balance is zero — top up on the billing page.
403 scope_deniedKey is scoped and the endpoint is out of scope.
404 not_foundThe record doesn't exist (and has never existed in our cache).
410 goneThe record was suppressed under a DSR — see Compliance.
422 extension_requiredCache miss on an extension-backed read; the LinkFetch Chrome extension needs to capture this row.
429 rate_limit / outbound_throttledSee Rate limits.
5xx internal_errorOur problem — request_id already paged us.

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.

{
  "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 handles the round-trip automatically when you run the call from the playground — you don't have to wire the bridge yourself unless you're embedding the flow in your own app.

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.

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

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

{
  "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

  • 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

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