Errors
One envelope, a fixed vocabulary, and one promise about billing.
Every failure has the same shape, whatever went wrong and however far into the stack it got.
{
"error": {
"type": "unprocessable_target",
"message": "Profile is private, deleted or unreachable.",
"requestId": "req_8f2ka91x"
}
}Switch on type, never on message. The type is the contract and will not be repurposed or re-cased once shipped; the wording exists for a human reading a log and can improve.
requestId is on every response, success or failure. It is the one thing a support conversation can start from, so keep it in your logs.
The vocabulary #
| Status | type | Means |
|---|---|---|
| 400 | invalid_request | The body or a parameter could not be accepted. error.field names it where there is one. |
| 401 | invalid_api_key | Missing, malformed, revoked or expired key. |
| 402 | insufficient_credits | The prepaid credit balance cannot cover the submission. Its own status because its own remedy: quota_exhausted clears when the month turns and this one clears when credits are bought, so a client retrying on a schedule would retry this forever. Top up and the same request runs. quota.balance on GET /account is the number to watch. |
| 403 | quota_exhausted | This month's allowance cannot cover the submission. |
| 403 | insufficient_scope | The key is valid and is the wrong one: it does not carry the scope this route needs. The message names the scope. Never retry it - mint a key that carries it. See Authentication. |
| 403 | email_unverified | Nobody on the account has confirmed their email address, and the account is on the default allowance rather than an agreed one. Confirm it and the same request works; waiting for the month to turn does nothing. |
| 404 | not_found | No such batch, or it belongs to another organization. The two are deliberately indistinguishable. |
| 409 | conflict | The batch has already finished. |
| 422 | unprocessable_target | Well-formed, but the target could not be acted on. |
| 422 | capacity_unavailable | We had no capacity able to run the item right now. Every operation can answer it; the message narrows it, because a Sales Navigator search needs capacity of a kind the others do not and can be short while everything else is fine. You get this rather than a thinner result, and credits_used is 0 - retrying later costs you only the wait. |
| 429 | rate_limited | Too many requests, or too many batches in flight. Carries Retry-After. |
| 500 | internal_error | Ours. The requestId is how we find it. |
| 501 | not_implemented | No operation returns this today - every one is answerable. It stays in the list because it is still a valid error.type, and it is what a newly documented operation would answer before its scraper lands. |
| 504 | upstream_timeout | LinkedIn did not answer in time. |
The billing promise #
If you did not get a record, you were not charged for it. Every error above is unmetered, and a failed result entry always carries credits_used: 0 - enforced by a database constraint rather than by a code path remembering.
Per-item errors #
A result entry uses the same vocabulary, minus the types that can only apply to a whole request. One failed item never fails a batch.
{
"item_index": 1,
"input": { "urn": "ACoAAA8BYqEBCGLg" },
"status": "failed",
"credits_used": 0,
"error": { "type": "unprocessable_target", "message": "Profile is private, deleted or unreachable." }
}Retrying #
- 429 and 5xx are worth retrying, with backoff. Honour Retry-After when it is there.
- 4xx other than 429 will fail again identically. Fix the request.
- Always retry a create with the same Idempotency-Key, so a lost response cannot become a duplicate batch.