New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
API Reference (v2)
Errors & Rate Limits
🔌

Audience: developers. API keys need a Pro or Enterprise plan (legacy Pro and Scale plans included) or an AppSumo Tier 5, 6 or 7 license, and they never work during a trial. Support agent: confirm the user's plan grants API access before giving API advice.

TL;DR: Endpoint errors are { "error": { code, problem, fix, retryable, docs_url, request_id, ... } } with a lowercase code. Authentication, plan and rate-limit errors use a shorter { "error": { code, message } } shape with an uppercase code. Branch on code, not on text. Rate limits are 60/min per organization (30 for AppSumo-only), 30/min per key, and 60 People Finder searches per UTC day for API keys. On a 429, wait for Retry-After.

Errors & Rate Limits

Error shapes

API v2 returns errors in one of three shapes. All three put the details under a top-level error object.

1. The public error envelope

Errors raised by an endpoint (validation, quotes, credits, scopes, not found, conflicts, provider failures):

{
  "error": {
    "code": "insufficient_credits",
    "problem": "This enrich_list call requires 110 credits; organization has 40.",
    "fix": "Top up at app.cleanlist.ai/billing, or reduce the scope of the call.",
    "retryable": false,
    "docs_url": "https://docs.cleanlist.ai/errors/insufficient-credits",
    "request_id": "req_4f1c2a9b3d6e7f80",
    "estimated_cost_credits": 110,
    "available_credits": 40,
    "shortfall_credits": 70
  }
}
FieldAlways presentMeaning
codeyesLowercase machine-readable code; branch on this
problemyesHuman-readable description
causenoWhat triggered it
fixnoSuggested remedy
retryableyesWhether retrying the same request could succeed
retry_after_msnoBackoff hint in milliseconds, mirrored to the Retry-After header
docs_urlyeshttps://docs.cleanlist.ai/errors/<code-with-dashes>, which opens the code's row below
request_idyesCorrelation id; include it when you contact support@cleanlist.ai
estimated_cost_credits, available_credits, shortfall_creditsnoPresent on credit errors
suggested_actionsnoFollow-up actions an agent can show or call
detailsnoExtra structured context (for example required and granted scopes)

Fields with no value are left out of the body.

Some fix strings say "Top up at app.cleanlist.ai/billing". They are returned verbatim by the API, but that page doesn't exist: buy credits or change plan on the Plans page at app.cleanlist.ai/plans (opens in a new tab) (Settings → Plans & billing). Likewise, a fix that says "Settings → Integrations" means the Integrations page in the app's sidebar.

2. Request body validation (422)

When the JSON body doesn't match the endpoint's schema (wrong types, unknown fields, missing required fields, or both halves of a one-of body), the request is rejected before the handler runs with HTTP 422 in the public envelope, code validation_error (lowercase), problem: "Request body failed validation." and the per-field errors in details.errors:

{
  "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.",
    "retryable": false,
    "docs_url": "https://docs.cleanlist.ai/errors/validation-error",
    "request_id": "req_...",
    "details": { "errors": [ { "type": "extra_forbidden", "loc": ["body", "domain"], "msg": "Extra inputs are not permitted" } ] }
  }
}

Checks that run inside the handler (an unknown filter key, an unknown tool in /credits/estimate) return the same validation_error code with HTTP 400. This shape is also used by the legacy v1 endpoints.

3. The generic shape (401, 403 plan blocks, 429)

Authentication failures, plan and trial blocks, and rate limits are raised before the endpoint runs and use a shorter shape with an uppercase code and a message (no problem, docs_url or request_id):

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Public API is limited to 60 requests per minute per organization.",
    "trace_id": "..."
  }
}

Plan blocks put the reason in details, for example {"error": "feature_gated", "feature": "api_access", "current_tier": "starter", "required_tier": "pro"}. Robust clients read error.code from either shape and compare it case-insensitively.

Error code reference

Codes in the public envelope, grouped by what went wrong. The anchor for each code is the code with dashes, so docs_url links land on the right row.

Request and validation

CodeHTTPRetryableMeaning and fix
validation_error400 or 422noThe body or parameters failed validation (unknown filter key, bad enum value, empty list, missing prompt for custom_ai). Read problem, fix and details
invalid_cursor400noThe pagination cursor is malformed. Drop it and start from the first page
invalid_criteria400noA field in the criteria search form has an unsupported value. details.field names it
empty_criteria400noThe criteria compile to no filter at all. Set at least one field
unsupported_exclusions400noThe criteria ask for an exclusion the search can't apply
crm_not_connected400noA CRM exclusion or CRM read needs a CRM that isn't connected. Connect it on the Integrations page
approval_required400noSync was called without approved: true, or a smart-agent selection is above 500 leads. Confirm with the user, or narrow the selection

Quotes and credits

CodeHTTPRetryableMeaning and fix
quote_required400noThis operation needs a signed quote_id. Call POST /credits/estimate first
quote_expired400noThe quote is older than 5 minutes. Estimate again
quote_invalid400noThe quote's signature or format is invalid. Estimate again
quote_mismatch400noThe quote was priced for a different request shape (list, scope, destination, mapping). Estimate the exact request
quote_already_redeemed409 (400 on enrichment)noQuotes are single use. Estimate again for a fresh one
spend_cap_exceeded400noThe real cost would exceed the quote, usually because the list grew. Estimate again
quote_verification_unavailable503yesThe server couldn't confirm the quote is unused. Retry shortly with the same quote
insufficient_credits400 (402 on list find-people)noYour balance can't cover the operation. See shortfall_credits; buy credits on the Plans page (app.cleanlist.ai/plans)

Access, plan and limits

CodeHTTPRetryableMeaning and fix
insufficient_scope403noYour credential lacks the endpoint's scope. details lists required and granted scopes; check GET /whoami
search_quota_exhausted403noThe workspace has used its search allowance for the period (AppSumo: 50 a month). Wait for the reset, or move to a paid plan for unlimited search
feature_gated403noThe feature isn't on your plan, or isn't available to AppSumo-only workspaces (for example find-people jobs)
list_protected403noThe list is protected (such as Extension Leads) and can't be renamed or deleted
list_not_editable403noThe list is shared with you as view-only
list_cap_reached403noYour plan's number of lists is reached. Delete a list or upgrade
lead_cap_reached403noThe list would go over your plan's contacts-per-list limit. Use another list or upgrade

Not found

CodeHTTPRetryableMeaning and fix
not_found404noThe resource doesn't exist or isn't in your workspace. Also returned for request fields that aren't enabled on your workspace, such as criteria
list_not_found404noThe lead list doesn't exist or you can't access it
task_not_found404noThe task_id cohort handle expired or isn't yours. Run the search again
folder_not_found404noThe folder doesn't exist or isn't yours

Resources in another organization are reported as 404 (not 403), so the API can't be used to discover other workspaces' data.

Conflicts

CodeHTTPRetryableMeaning and fix
conflict409noThe selection changed between quote and run (for example, leads were added to the list), or an import is still processing. Refresh and estimate again
search_result_conflict409noA saved profile has a different company or role from the search result you're saving

Providers, sync and server

CodeHTTPRetryableMeaning and fix
provider_not_connected400noThe CRM or Lemlist isn't connected to this workspace. Connect it on the Integrations page, then estimate again
provider_error502noAn upstream data provider returned an error. Try again later
filters_unavailable503yesThe filter source is temporarily unavailable. Retry with backoff
dispatch_outcome_unknown503noThe workflow service didn't confirm whether enrichment started. Check status before retrying so you don't run it twice
not_enabled503noThe feature is switched off for maintenance (for example CSV import). Try later
feature_not_available501noThe requested smart-agent type isn't enabled
not_supported501noThe operation isn't supported for this provider
internal_error500, 502 or 503variesUnexpected server error. Retry with backoff; if it persists, contact support with the request_id

Codes from routes the Cleanlist MCP uses

These come from v2 routes built for the Cleanlist MCP (folders, skills, CRM reads, filter values). You'll mostly see them in Claude.

CodeHTTPRetryableMeaning and fix
rate_limited429yesToo many skill runs started at once (20 a minute per organization). Wait for Retry-After
crm_budget_exhausted429yesThe workspace has used its daily CRM API allowance. It resets at 00:00 UTC
run_not_found404noThe skill run doesn't exist or isn't yours
unknown_section404noThe reference section doesn't exist
folder_not_empty409noMove or delete the folder's lists first
idempotency_conflict409noThe same idempotency key was reused with a different request
forbidden403noYou don't have permission for this action
prompt_too_long400noThe skill prompt is over 8,000 tokens. Shorten it
query_required400noA filter-values lookup needs a query
unknown_field400noThe filter-values lookup named an unknown field
unknown_object_type400noThe CRM record type isn't supported

Generic codes (uppercase)

CodeHTTPWhen
AUTHENTICATION_INVALID401Missing header ("Not authenticated"), or an invalid, expired or revoked key ("Invalid or expired API key")
PERMISSION_DENIED403Plan without API access (details.error: "feature_gated"), a key used during a trial ("Public API access is not included during the trial..."), or a feature not on the plan
RATE_LIMITED429Any rate limit in the table below; comes with Retry-After
RESOURCE_NOT_FOUND404A route that isn't available to your workspace, or a legacy v1 lookup that found nothing
BAD_REQUEST400Legacy v1 request errors
UPSTREAM_ERROR502Legacy v1 provider failure

Older versions of these docs listed invalid_token, workflow_timeout, version_deprecated, invalid_filter_id, estimate_token_mismatch and field_mapping_required. The API doesn't return any of them.

Rate limits

These limits apply to every API v2 request at the same time:

LimitWindowScope
60 requestsper minuteper organization (all keys and members combined)
30 requestsper minuteper organization, for AppSumo licenses without a paid Cleanlist plan (instead of 60)
30 requestsper minuteper clapi_ API key

Signed-in app sessions are held to the per-organization limit but not the per-key limit.

People Finder has its own limits

POST /people/find is metered on top of the limits above:

CallerLimit
API keys60 searches per UTC day, per organization, shared by all keys
Cleanlist MCP60 searches per minute, per organization

The daily cap is the one most integrations hit first. A backfill that runs one search per record runs out after 60 records and gets 429 until midnight UTC, with the message "People Search rate limit exceeded. Public API keys are limited to 60 searches per day per organization. Use the Cleanlist MCP for higher throughput." Each cursor page counts as a search. Pull bigger pages (up to 100 rows each) rather than many narrow searches, or use the Cleanlist MCP for higher volume. Company search and find-at-companies don't count toward this cap.

Handling 429

A rate-limited request returns HTTP 429 in the generic shape:

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Per-API-key rate limit exceeded. Each API key is limited to 30 requests per minute. Spread traffic across keys or wait."
  }
}

A standard Retry-After header (in seconds) comes with every 429, so generic HTTP clients can back off without parsing the body. Honor it, then retry:

import time, requests
 
def call_with_backoff(method, url, **kw):
    while True:
        r = requests.request(method, url, **kw)
        if r.status_code != 429:
            return r
        wait = int(r.headers.get("Retry-After", "2"))
        time.sleep(wait)
async function callWithBackoff(url, opts) {
  while (true) {
    const r = await fetch(url, opts);
    if (r.status !== 429) return r;
    const wait = parseInt(r.headers.get("Retry-After") || "2", 10);
    await new Promise((res) => setTimeout(res, wait * 1000));
  }
}

For the daily People Finder cap, Retry-After can be many hours; stop and resume after midnight UTC instead of sleeping in a loop.

Learn more