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

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: API v2 is poll-based; it has no webhooks. Async calls return an id; poll the matching status endpoint until the status is terminal. Chain operations with the task_id cohort handle returned by searches and list reads. Status polls cost no credits. (If you need push delivery, the legacy v1 API offers webhooks on its enrich/bulk endpoint.)

Polling & Async Results

Several API v2 operations run in the background. This page explains how to get their results.

API v2 has no webhook or callback endpoint and no webhook_url parameter; the supported pattern is polling. If you need push delivery, the separate legacy v1 API accepts a webhook_url on its enrich/bulk endpoint and keeps a delivery log.

Which operations are async?

OperationReturnsPollTerminal statuses
POST /enrichment/personworkflow_idGET /enrichment/status/{workflow_id}completed, completed_with_errors, failed, canceled, terminated
POST /enrichment/bulkworkflow_idGET /enrichment/status/{workflow_id}completed, completed_with_errors, failed
POST /enrichment/by-task (person)workflow_idGET /enrichment/status/{workflow_id}same as bulk
POST /lead-lists/{id}/csv-import (dispatching)workflow_id + enrichment_status_urlGET /enrichment/status/{workflow_id}same as bulk
POST /smart-agents/runsmart_agent_task_idGET /smart-agents/{smart_agent_task_id}completed, failed
POST /people/find-at-companiestask_id + poll_urlGET /people/find-at-companies/status/{task_id}completed, failed
POST /lead-lists/{id}/find-peopleworkflow_id + poll_urlGET /lead-lists/{id}/find-people/status/{workflow_id}completed, failed, cancelled

Synchronous operations (company enrichment, by-task for companies, CRM and sequencer sync and exports) return their results directly and need no polling. Sync responses include a workflow_id for consistency, but the counts are already final.

Spelling differs by job: enrichment uses canceled (one l), find-people uses cancelled (two). Compare against the exact values in the table.

Polling enrichment status

GET /enrichment/status/{workflow_id}

Scope: enrich:read · Cost: free

Poll until status is terminal: completed, completed_with_errors or failed. Single-record runs can also end canceled or terminated. A bulk run with any failed rows settles as completed_with_errors, not completed, so a loop that only checks for completed never exits; always treat completed_with_errors as terminal. The non-terminal states are pending and processing. The handle can be a workflow_id (enrich-..., bulk-enrich-...) or a cl-task_... cohort handle.

import time, requests
 
H = {"Authorization": "Bearer clapi_your_api_key"}
B = "https://api.cleanlist.ai/api/v2"
TERMINAL = ("completed", "completed_with_errors", "failed", "canceled", "terminated")
 
def wait_for(workflow_id, interval=5, timeout=900):
    deadline = time.time() + timeout
    while time.time() < deadline:
        s = requests.get(f"{B}/enrichment/status/{workflow_id}", headers=H).json()
        if s["status"] in TERMINAL:
            return s
        time.sleep(interval)
    raise TimeoutError(workflow_id)
const B = "https://api.cleanlist.ai/api/v2";
const H = { Authorization: "Bearer clapi_your_api_key" };
const TERMINAL = ["completed", "completed_with_errors", "failed", "canceled", "terminated"];
 
async function waitFor(workflowId, interval = 5000, timeout = 900000) {
  const deadline = Date.now() + timeout;
  while (Date.now() < deadline) {
    const s = await (await fetch(`${B}/enrichment/status/${workflowId}`, { headers: H })).json();
    if (TERMINAL.includes(s.status)) return s;
    await new Promise((r) => setTimeout(r, interval));
  }
  throw new Error(`timeout: ${workflowId}`);
}

See Enrichment: Status for the full response, including the inline result for single-person runs and the settled credits_charged and credits_refunded.

Polling tips

  • Free, but within rate limits. Status polls cost no credits but count toward the 60 requests per minute per organization (and 30 per key). Poll every few seconds; back off for long bulk jobs.
  • Read the counters. total, processed, completed and failed show live progress on bulk runs.
  • Watch refund_status. A terminal failure on a prepaid run sets refund_status: "pending_review" with a message to contact support; show it to the user.
  • Bulk results aren't inline. For bulk and cohort runs, read the enriched leads from the target list with GET /lead-lists/{id}/leads once the workflow completes.
  • Smart-agent results grow. GET /smart-agents/{id} returns the per-lead results that are finished so far, so a running job returns a partial set.

Chaining with task_id

Many responses carry a task_id in the envelope, a short-lived cohort handle. It's the glue for multi-step workflows:

  1. Search: POST /people/find returns rows and a task_id for the result cohort.
  2. Save or enrich the cohort: pass that task_id to POST /lead-lists/{id}/leads (free), or to POST /enrichment/by-task with a quote. By-task returns a workflow_id.
  3. Poll: poll the workflow_id (or the cl-task_... handle) with GET /enrichment/status until terminal.

The same works for GET /lead-lists/{id}/leads, which returns a task_id for the page so you can enrich a page of an existing list the same way.

Cohort handles belong to your organization. They last about an hour, refreshed each time you use them, with a hard 24-hour limit. If a task_id has expired (404 task_not_found), run the original search again to get a fresh one.

Learn more