New: API Reference docs are live — integrate Cleanlist enrichment into your apps. View API docs →
API Reference
Errors & Rate Limits

TL;DR: Errors come back as { "detail": "..." } JSON. The most important codes to handle: 401 (bad key), 402 (out of credits), 422 (validation), 429 (rate limit).

Errors

The Cleanlist Public API uses standard HTTP status codes and FastAPI's default JSON error format.

Error format

Most errors look like this:

{
  "detail": "Human-readable error message"
}

422 validation errors include a structured array describing each invalid field:

{
  "detail": [
    {
      "type": "value_error",
      "loc": ["body", "contacts", 0],
      "msg": "Each contact must include linkedin_url OR first_name + last_name + (company_domain or company_name).",
      "input": {"first_name": "John"}
    }
  ]
}

Status code reference

CodeMeaningWhen you'll see it
200OKSuccessful GET / POST
400Bad RequestEmpty contacts, more than 250 contacts, supplying both workflow_id and task_id
401UnauthorizedMissing, malformed, expired, or revoked API key
402Payment RequiredInsufficient credits
403ForbiddenYour plan does not include API access
404Not FoundWorkflow id, task id, or webhook id does not exist
422Unprocessable EntityPydantic validation error (bad field types, missing required combos)
429Too Many RequestsPublic API rate limit exceeded
500Internal Server ErrorUnexpected backend failure — please report it
503Service UnavailableTemporary backend outage; retry with backoff

Common errors and how to fix them

401 Unauthorized

{ "detail": "Not authenticated" }
CauseFix
Missing Authorization headerAdd Authorization: Bearer clapi_...
Wrong scheme (e.g., X-API-Key or basic auth)Use Bearer
Key was revokedGenerate a new one in the portal
Key has expiredGenerate a new one with a longer / no expiration

Confirm with GET /api/v1/public/auth/validate-key.

402 Payment Required

{ "detail": "Insufficient credits" }
CauseFix
Organization credit balance is below the cost of the requestTop up in the portal (opens in a new tab) under Settings → Billing

Failed enrichments cost zero credits, so retries after a top-up are safe.

400 Bad Request

BodyCause
"At least one contact is required."You sent an empty contacts array
"Bulk enrichment supports up to 250 contacts per request."You sent more than 250 contacts
"Provide exactly one of workflow_id or task_id."You hit /enrich/status with both, or with neither

Split large batches client-side; the 250-contact cap is hard.

422 Unprocessable Entity

The most common validation failure is a contact missing the required field combo. Each contact must include either:

  • linkedin_url, or
  • first_name + last_name + (company_domain or company_name)

Inspect detail[].loc to find which contact index is offending:

{
  "detail": [
    {
      "type": "value_error",
      "loc": ["body", "contacts", 7],
      "msg": "Each contact must include linkedin_url OR first_name + last_name + (company_domain or company_name)."
    }
  ]
}

In this example, contacts[7] is the bad row.

429 Too Many Requests

{ "detail": "Rate limit exceeded" }

The Public API enforces per-organization, per-endpoint rate limits to keep the platform fair. Limits are designed for normal automation patterns and are not published — back off on 429 and you should never hit them in practice.

Recommended retry strategy

import time
import random
import requests
 
def call_with_backoff(method, url, **kwargs):
    for attempt in range(6):  # 1 try + 5 retries
        r = requests.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        sleep_for = (2 ** attempt) + random.uniform(0, 0.5)
        time.sleep(sleep_for)
    r.raise_for_status()

Use exponential backoff with jitter. Avoid tight loops on the same endpoint.

500 Internal Server Error

Something on Cleanlist's side broke. The response body usually includes a short reason. Treat it like a transient failure: retry with backoff. If it persists, email support with the request id (visible in your portal API request log) and we'll investigate.

Webhook delivery errors

Webhook deliveries are tracked separately from the API call that submitted them. A successful POST /enrich/bulk returning 200 does not guarantee that the webhook was delivered — those are separate events.

To inspect delivery results, query GET /api/v1/public/webhooks/deliveries?workflow_id=.... Each row's status is either delivered or failed, with the response code and error message attached. See Webhooks for the full schema.

Defensive coding patterns

def safe_enrich(payload, api_key):
    r = requests.post(
        "https://api.cleanlist.ai/api/v1/public/enrich/bulk",
        headers={"Authorization": f"Bearer {api_key}"},
        json=payload,
    )
 
    if r.status_code == 401:
        raise RuntimeError("Cleanlist API key is invalid or revoked")
    if r.status_code == 402:
        raise RuntimeError("Out of credits — top up at portal.cleanlist.ai")
    if r.status_code == 422:
        # Surface field-level errors so the caller can fix the input
        raise ValueError(r.json()["detail"])
    if r.status_code == 429:
        # Retry with backoff
        raise TransientError("Rate limited")
 
    r.raise_for_status()
    return r.json()

Related