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: 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. For push delivery, subscribe to webhook events such as enrichment.completed and play_run.completed.
Polling & Async Results
Several API v2 operations run in the background. This page explains how to get their results.
The async calls take no callback URL; they return an id you poll. To be told when work finishes, subscribe a URL to webhook events (enrichment.completed, play_run.completed, play_run.failed and more) and read the results with the ids in the event. The separate legacy v1 API also accepts a per-run webhook_url on its enrich/bulk endpoint.
Which operations are async?
| Operation | Returns | Poll | Terminal statuses |
|---|---|---|---|
POST /enrichment/person | workflow_id | GET /enrichment/status/{workflow_id} | completed, completed_with_errors, failed, cancelled, canceled, terminated |
POST /enrichment/bulk | workflow_id | GET /enrichment/status/{workflow_id} | completed, completed_with_errors, failed, cancelled, canceled, terminated |
POST /enrichment/by-task (person) | workflow_id | GET /enrichment/status/{workflow_id} | same as bulk |
POST /lead-lists/{id}/csv-import (dispatching) | workflow_id + enrichment_status_url | GET /enrichment/status/{workflow_id} | same as bulk |
POST /smart-agents/run | smart_agent_task_id | GET /smart-agents/{smart_agent_task_id} | completed, failed |
POST /people/find-at-companies | task_id + poll_url | GET /people/find-at-companies/status/{task_id} | completed, failed |
POST /lead-lists/{id}/find-people | workflow_id + poll_url | GET /lead-lists/{id}/find-people/status/{workflow_id} | completed, failed, cancelled |
POST /migrate/crm, POST /migrate/sequencer (more than 500 leads) | workflow_id with status: "pending" | GET /migrate/status/{workflow_id} | completed, failed |
POST /export/csv/signed-url (HTTP 202) | job_id with status: "running" | POST /export/csv/signed-url again with the same list_id and the job_id | completed (with download_url) |
Synchronous operations (company enrichment, by-task for companies and the JSON export) return their results directly and need no polling. CRM and sequencer sync of up to 500 leads also returns final counts in the response; check status, because a bigger list comes back pending. A CSV export waits up to 20 seconds for the file; if it isn’t ready you get HTTP 202, and each call with the job_id waits up to 20 seconds again.
Spelling differs: a stopped enrichment reads cancelled (two ls) while its run is cached, and canceled (one l) or terminated once the cache has expired and the status comes from the workflow itself. Find-people uses cancelled. Treat every spelling in the table as terminal.
Polling enrichment status
GET /enrichment/status/{workflow_id}
Scope: enrich:read · Cost: free
Poll until status is terminal: completed, completed_with_errors or failed. A run that was stopped ends cancelled, 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", "cancelled", "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", "cancelled", "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,completedandfailedshow live progress on bulk runs. - Watch
refund_status. A terminal failure on a prepaid run setsrefund_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}/leadsonce the workflow completes. - Smart-agent results grow.
GET /smart-agents/{id}returns the per-leadresultsthat 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:
- Search:
POST /people/findreturns rows and atask_idfor the result cohort. - Save or enrich the cohort: pass that
task_idtoPOST /lead-lists/{id}/leads(free), or toPOST /enrichment/by-taskwith a quote. By-task returns aworkflow_id. - Poll: poll the
workflow_id(or thecl-task_...handle) withGET /enrichment/statusuntil 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.