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.
Where
Settings→API & Integrations
Your role needs
Update access to Lead intake API
update-companies. Admin have it by default.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.
Status and error codes
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.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 a401 with error: "invalid_api_key", whatever caused it:
401 Unauthorized
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:
401 Unauthorized: orphaned key
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, 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 a403, naming exactly what’s missing:
403 Forbidden
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:
404 Not Found
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:
409 Conflict: duplicate lead
lead_id is the existing lead, so you can fetch or update it directly instead of retrying the create. See 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:
409 Conflict: idempotency key reused
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:
422: missing contact detail
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 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.
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:
One row inside a 200 batch response
error holds the message, not one of the codes from the table above: those codes are top-level fields, never row fields.
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:
An unhandled row failure
Related
POST /leads
Request body, duplicate handling and the full response envelope.
Create, roll and revoke API keys
Create a key, copy its secret once, and revoke it when you need to.
Idempotency, deduplication and rate limits
Retry safely, and see what happens when you send too fast.