TL;DR: The MCP API is poll-based — it has no webhook endpoint. Async work returns a workflow_id; retrieve results by polling GET /enrichment/status/{workflow_id} until the status is terminal. Chain operations with the task_id cohort handle returned by searches and list reads. Status polls are free. (If you need push delivery, the legacy Public API offers webhooks on its enrich/bulk endpoint.)
Polling & Async Results
Several Cleanlist operations run in the background. This page explains how to retrieve their results.
The MCP API has no webhook / callback endpoint and no webhook_url request parameter — the supported pattern is polling. If you specifically need push delivery, the separate legacy Public API supports a webhook_url on its enrich/bulk endpoint with a delivery audit log.
Which operations are async?
| Operation | Returns | Result location |
|---|---|---|
POST /enrichment/person | workflow_id | the target lead_list_id + inline result on terminal status |
POST /enrichment/bulk | workflow_id | the enriched list |
POST /enrichment/by-task (person) | workflow_id | the target list (or the auto "MCP Results" list) |
POST /lead-lists/{id}/csv-import (dispatching) | workflow_id + enrichment_status_url | the imported list |
POST /smart-agents/run | smart_agent_task_id | poll GET /smart-agents/{id} |
Synchronous operations — company enrichment, by-task (company), CRM/sequencer sync, exports — return their results directly and need no polling.
Polling enrichment status
GET /enrichment/status/{workflow_id}
Scope: enrich:read · Cost: free
Poll until status is completed, failed, or cancelled. The handle accepts either 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/v1/public"
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 ("completed", "failed", "cancelled"):
return s
time.sleep(interval)
raise TimeoutError(workflow_id)const B = "https://api.cleanlist.ai/api/v1/public";
const H = { Authorization: "Bearer clapi_your_api_key" };
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 (["completed", "failed", "cancelled"].includes(s.status)) return s;
await new Promise((r) => setTimeout(r, interval));
}
throw new Error(`timeout: ${workflowId}`);
}See Enrichment → Status for the full response schema, including the inline result for single-person runs and the settled credits_charged / credits_refunded fields.
Polling tips
- Free, but be reasonable. Status polls cost no credits. Poll every few seconds; back off for long bulk jobs.
- Read the counters.
total,processed,completed, andfailedshow 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 — surface it to the user. - Bulk results aren't inline. For bulk and cohort runs, read the enriched leads from the target list via
GET /lead-lists/{id}/leadsonce the workflow completes.
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 agent workflows:
- Search —
POST /search/peoplereturns rows and atask_idfor the result cohort. - Enrich the cohort — pass that
task_idtoPOST /enrichment/by-taskwithout re-listing everylead_id. That returns aworkflow_id. - Poll — poll the
workflow_id(or thecl-task_...handle) viaGET /enrichment/statusuntil terminal.
The same applies to GET /lead-lists/{id}/leads, which emits a task_id for the page so you can enrich a page of an existing list the same way.
Cohort handles are org-scoped and expire after roughly an hour. If a task_id has expired, re-run the originating search to obtain a fresh one.