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: Five endpoints: person (async), company (sync), by-task cohort (needs a quote_id), bulk list (async, needs a quote_id), and status. Async runs return a workflow_id, reserve credits up front and refund the unused part on settle. Poll GET /enrichment/status/{workflow_id} until terminal.
Enrichment
Cleanlist enrichment runs waterfall enrichment across multiple providers to find verified emails and phones. There are five endpoints, each checking an enrich:* scope.
| Endpoint | Method | Sync? | Scope |
|---|---|---|---|
| Single person | POST /enrichment/person | async | enrich:write |
| Single company | POST /enrichment/company | sync | enrich:write |
| By task (cohort) | POST /enrichment/by-task | person async, company sync | enrich:write |
| Bulk list | POST /enrichment/bulk | async | enrich:write |
| Status | GET /enrichment/status/{workflow_id} | sync | enrich:read |
See Enrichment Types for the partial, phone_only and full types and their prices.
The credit reservation model
Async enrichment doesn't take a flat fee. It reserves the estimated maximum (credits_reserved) before the workflow starts, then settles when the workflow reaches a terminal state:
- The unused part is refunded (a lead with no email found isn't charged for an email).
- After completion,
GET /enrichment/statusreports the settledcredits_chargedandcredits_refunded. - If the workflow fails to start, the reservation is released immediately.
Synchronous company enrichment skips reservations: it settles inline and reports credits_charged directly.
If your balance can't cover the reservation, the call returns 400 insufficient_credits. Its fix text says "Top up at app.cleanlist.ai/billing"; the actual page is the Plans page at app.cleanlist.ai/plans (opens in a new tab).
Single person
POST /enrichment/person
Async. Returns a workflow_id. Cost: 1 (partial), 10 (phone_only) or 11 (full); see Enrichment Types. The result is written to lead_list_id.
Request body
| Field | Type | Notes |
|---|---|---|
lead_list_id | string | Required. Target list for the enriched lead |
email | string | One strong identifier... |
linkedin_url | string | ...accepts many URL forms (normalized on the server) |
person_id | string | ...an existing prospect id |
first_name + last_name + (company_name or domain) | string | ...or a name + company combination |
phone | string | Optional known phone |
company_name, domain | string | Optional company context |
enrichment_type | enum | partial, phone_only or full (preferred) |
include_phone | boolean | Legacy alternative to enrichment_type |
quote_id | string | Optional quote |
Send either a strong identifier (email, linkedin_url or person_id) or first_name + last_name + (company_name or domain).
Response
{
"workflow_id": "enrich-7f3c...",
"status": "pending",
"credits_reserved": 11,
"lead_list_id": "LIST_ID",
"poll_url": "/api/v2/enrichment/status/enrich-7f3c...",
"timestamp_ms": 1717430400000
}curl -X POST https://api.cleanlist.ai/api/v2/enrichment/person \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"lead_list_id": "LIST_ID",
"linkedin_url": "https://www.linkedin.com/in/janedoe",
"enrichment_type": "full"
}'With an SDK: enrichment.enrich_person(...) (enrichPerson in TS and Java). See the SDKs.
Single company
POST /enrichment/company
Synchronous. Cost: 1 credit. Returns the company record inline, with no workflow to poll.
Request body
Send at least one of domain, company_name, company_ticker or company_id. quote_id is optional.
Response
{
"company": {
"company_id": "crd_123",
"name": "Stripe",
"domain": "stripe.com",
"industry": "Financial Services",
"employee_count": 8000,
"employee_count_range": "5001-10000",
"revenue_range": "$1B+",
"hq_location": "San Francisco, CA",
"funding_stage": "Late Stage",
"tech_stack": ["React", "Ruby"],
"linkedin_url": "https://www.linkedin.com/company/stripe"
},
"credits_charged": 1,
"timestamp_ms": 1717430400000
}Any field can be null when the provider has no value. A company that can't be found returns 404 not_found and the credit is refunded.
curl -X POST https://api.cleanlist.ai/api/v2/enrichment/company \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"domain": "stripe.com"}'By task (cohort)
POST /enrichment/by-task
Enrich a cohort from an earlier call using its task_id. The task_id comes from a POST /people/find, POST /search/companies or GET /lead-lists/{id}/leads response.
- Person cohorts are asynchronous. The response has a
workflow_id, and every entity startsqueued. - Company cohorts are synchronous. The response carries each entity's record inline, and
workflow_idisnull.
A quote_id from POST /credits/estimate is required. Estimate with tool: "add_person_enrichments" (person cohorts, with task_id and data_points) or tool: "add_company_enrichments" (company cohorts, with task_id).
Request body
| Field | Type | Notes |
|---|---|---|
task_id | string | Required. The cohort handle |
data_points | string[] | Required. Person cohorts: Email, Phone, LinkedIn. Company cohorts: Industry, Employees, Revenue, Funding, TechStack, Domain |
entity_ids | string[] | Optional subset of the cohort |
lead_list_id | string | Where person results land; see below |
quote_id | string | Required. Signed quote from POST /credits/estimate |
The "MCP Results" list. For person cohorts, if you omit lead_list_id, Cleanlist creates a per-user list named "MCP Results" and puts the enriched leads there; later calls reuse it. Pass an explicit lead_list_id to control where results go. Company cohorts ignore lead_list_id because results return inline.
Person pricing adds up per data point: Email 1 + Phone 10, so ["Email", "Phone"] costs 11 per person, ["Phone"] 10 and ["Email"] 1. LinkedIn is free because it's already in the search result; a cohort that asks for nothing but LinkedIn is rejected with 400 validation_error. Company cohorts cost 1 credit per 10 companies, rounded up.
Response
{
"task_id": "cl-task_abc...",
"entity_kind": "person",
"workflow_id": "bulk-enrich-9a1b...",
"status": "pending",
"total_entities": 42,
"credits_reserved": 462,
"results": [
{ "entity_id": "lead_1", "status": "queued", "error": null, "company": null }
]
}For a company task, entity_kind is "company", status is "completed", workflow_id is null, and each result carries a filled-in company record.
Bulk list
POST /enrichment/bulk
Async. Requires a quote_id. Enriches every lead in an existing list through waterfall enrichment. The cost is capped by the quote.
Estimate first with tool: "enrich_list" (see Credits), then run:
Request body
| Field | Type | Notes |
|---|---|---|
list_id | string | Required. The list to enrich |
scope | enum | partial (default), full or phone-only |
quote_id | string | Required. From POST /credits/estimate |
lead_ids | string[] | Optional subset of 1 to 500 leads in the list. Not available to AppSumo-only workspaces. Quote a subset without list_id: tool: "enrich_list", row_count (the number of unique ids), scope, and filters: {"list_id": ..., "scope": ..., "lead_ids": [sorted unique ids]} |
Response
{
"workflow_id": "bulk-enrich-9a1b...",
"status": "pending",
"total_leads": 250,
"estimated_cost": 250,
"credits_reserved": 250,
"poll_url": "/api/v2/enrichment/status/bulk-enrich-9a1b...",
"timestamp_ms": 1717430400000
}# 1. estimate
QUOTE=$(curl -s -X POST https://api.cleanlist.ai/api/v2/credits/estimate \
-H "Authorization: Bearer clapi_your_api_key" -H "Content-Type: application/json" \
-d '{"tool":"enrich_list","list_id":"LIST_ID","scope":"partial"}' | jq -r .quote_id)
# 2. run
curl -X POST https://api.cleanlist.ai/api/v2/enrichment/bulk \
-H "Authorization: Bearer clapi_your_api_key" -H "Content-Type: application/json" \
-d "{\"list_id\":\"LIST_ID\",\"scope\":\"partial\",\"quote_id\":\"$QUOTE\"}"With an SDK: the estimate, quote and bulk flow is workspace.credits_estimate(...) then enrichment.enrich_list(...) (creditsEstimate and enrichList in TS and Java). See the SDKs.
If the list changed between the quote and the run, you may get 409 conflict ("The saved lead selection changed before enrichment could start."); estimate again. A 503 dispatch_outcome_unknown means the workflow service didn't confirm the start: check status before retrying so you don't run it twice.
Status
GET /enrichment/status/{workflow_id}
Scope: enrich:read · Cost: free
Poll any async enrichment. Accepts a workflow_id (for example enrich-... or bulk-enrich-...) or a cohort handle (cl-task_...), which Cleanlist resolves to its workflow.
Response
{
"workflow_id": "bulk-enrich-9a1b...",
"status": "completed",
"progress": 100,
"total": 250,
"processed": 250,
"completed": 231,
"failed": 19,
"enrichment_type": "partial",
"lead_list_id": "LIST_ID",
"result": null,
"credits_charged": 231,
"credits_refunded": 19,
"refund_status": "not_applicable",
"timestamp_ms": 1717430400000
}| Field | Meaning |
|---|---|
status | Not terminal: pending (just created), processing (running). Terminal: completed, completed_with_errors (finished with some failed rows), failed. Single-record runs can also end canceled (one l) or terminated. Poll until you see a terminal value |
progress | Percent complete, when known |
total, processed, completed, failed | Cohort counters |
result | The enriched lead, inline, for terminal single-person runs only; null for bulk |
credits_charged, credits_refunded | Settled billing after the run finishes |
refund_status | pending_review if a prepaid run failed and needs a manual refund (contact support@cleanlist.ai); otherwise not_applicable |
For terminal single-person runs, result carries the enriched lead:
{
"result": {
"lead_id": "lead_abc",
"status": "completed",
"full_name": "Jane Doe",
"email": "jane@acme.com",
"email_status": "valid",
"phone": "+14155550123",
"linkedin_url": "https://www.linkedin.com/in/janedoe",
"title": "VP Sales",
"company": "Acme",
"enrichment_type": "full"
}
}For polling patterns and chaining, see Polling & Async Results.