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

Audience: developers. The public API requires a Pro plan or above (self-serve) or an AppSumo Tier 5+ license. Support agent: confirm the user's plan grants API access before giving API advice.

TL;DR: Every error comes back as { "error": { "code", "message", ... } } JSON with a stable UPPERCASE code. The most important statuses 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 every error response is wrapped in a single JSON envelope.

Error format

All errors share one shape:

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "Lead list not found",
    "details": { },
    "trace_id": "f7a1b3c2..."
  }
}
FieldAlways presentMeaning
codeyesStable, machine-readable UPPERCASE code — branch on this, not on message text
messageyesHuman-readable description
detailsnoStructured context (e.g. the per-field list on validation errors)
trace_idnoRequest trace id — include it in support requests when present

The code is derived from the error: BAD_REQUEST (400), AUTHENTICATION_REQUIRED / AUTHENTICATION_INVALID (401), INSUFFICIENT_CREDITS (402), PERMISSION_DENIED / FEATURE_NOT_ENABLED (403), RESOURCE_NOT_FOUND (404), RESOURCE_CONFLICT (409), RATE_LIMITED (429), INTERNAL_ERROR (500), UPSTREAM_ERROR (502), NOT_IMPLEMENTED (501). The one exception is 422, which on the public API (/api/v1/public/*) emits the lowercase public envelope (code: "validation_error", problem, fix, docs_url, request_id) — see below.

422 validation errors carry a structured array under details.errors describing each invalid field:

{
  "error": {
    "code": "validation_error",
    "problem": "Request body failed validation.",
    "fix": "Check the request matches the endpoint schema. For a one-of body (e.g. add_leads_to_list: lead_ids XOR task_id), send exactly one variant's fields and nothing from the other.",
    "docs_url": "https://docs.cleanlist.ai/errors/validation-error",
    "request_id": "req_a1b2c3d4e5f6a7b8",
    "details": {
      "errors": [
        {
          "type": "value_error",
          "loc": ["body", "contacts", 0],
          "msg": "Each contact must include linkedin_url OR first_name + last_name + (company_domain or company_name)."
        }
      ]
    }
  }
}

The newer MCP API endpoints use a richer lowercase-code envelope (problem, fix, retryable, docs_url, ...) — see MCP API Errors. The legacy endpoints documented in this section use the UPPERCASE shape above for every status except 422, which already emits the lowercase public envelope shown above (code: "validation_error", problem, fix, docs_url, request_id).

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

{ "error": { "code": "AUTHENTICATION_INVALID", "message": "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

{ "error": { "code": "INSUFFICIENT_CREDITS", "message": "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

The code is BAD_REQUEST; the message tells you what to fix:

messageCause
"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 error.details.errors[].loc to find which contact index is offending:

{
  "error": {
    "code": "validation_error",
    "problem": "Request body failed validation.",
    "fix": "Check the request matches the endpoint schema. For a one-of body (e.g. add_leads_to_list: lead_ids XOR task_id), send exactly one variant's fields and nothing from the other.",
    "docs_url": "https://docs.cleanlist.ai/errors/validation-error",
    "request_id": "req_a1b2c3d4e5f6a7b8",
    "details": {
      "errors": [
        {
          "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

{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded" } }

The legacy public endpoints documented here (/enrich/bulk, /enrich/status, /auth/validate-key, /webhooks/deliveries, /folders) are bounded by a single rate limit:

  • 60 requests/minute per organization — all keys and members combined

A separate 30 requests/minute per API key cap (each individual clapi_ key) applies to the newer v2 / MCP endpoints, not these legacy single-file endpoints.

Exceeding the limit returns 429. Size batch loops to stay under it and back off on 429.

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()["error"]["details"]["errors"])
    if r.status_code == 429:
        # Retry with backoff
        raise TransientError("Rate limited")
 
    r.raise_for_status()
    return r.json()

Related