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
| 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. |
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. |
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. |
5xx internal_error | Our 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.