TL;DR: Five endpoints — person (async), company (sync), by-task cohort, bulk list (async, requires a quote_id), and status. Async runs return a workflow_id; reserve credits up front and refund the unused portion 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, all requiring 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 / full distinction and pricing.
The credit reservation model
Async enrichment doesn't deduct 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 portion 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.
Single person
POST /enrichment/person
Async. Returns a workflow_id. Cost: 1 (partial), 10 (phone_only), or 11 (full) — see Enrichment Types. The result lands in 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 forms (canonicalized server-side) |
person_id | string | …an existing prospect id |
first_name + last_name + (company_name or domain) | string | …or a name + company combo |
phone | string | Optional known phone |
company_name, domain | string | Optional company context |
enrichment_type | enum | partial | phone_only | full (preferred) |
include_phone | boolean | Legacy alternative to enrichment_type |
quote_id | string | Optional pre-committed quote |
You must provide 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/v1/public/enrichment/status/enrich-7f3c...",
"timestamp_ms": 1717430400000
}curl -X POST https://api.cleanlist.ai/api/v1/public/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"
}'Single company
POST /enrichment/company
Synchronous. Cost: 1 credit. Returns the full company record inline — no workflow to poll.
Request body
Provide 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
}A company that can't be found returns 404 not_found and the credit is refunded.
curl -X POST https://api.cleanlist.ai/api/v1/public/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 a prior search using its task_id. The task_id comes from a previous POST /search/people, POST /search/companies, or GET /lead-lists/{id}/leads response.
- Person tasks → asynchronous. Returns a
workflow_id; entity results are allqueued. - Company tasks → synchronous. Returns each entity's looked-up record inline;
workflow_idisnull.
No quote is required — the cohort is already scope-bound by the task's entity_ids.
Request body
| Field | Type | Notes |
|---|---|---|
task_id | string | Required. The cohort handle from a prior search/list read |
data_points | string[] | Required. For person tasks: Email, Phone, LinkedIn. For company tasks: Industry, Employees, Revenue, Funding, TechStack, Domain |
entity_ids | string[] | Optional subset of the task's cohort (must be a strict subset) |
lead_list_id | string | Where person results land; see below |
The "MCP Results" scratch list. For person cohorts, if you omit lead_list_id, Cleanlist auto-creates a per-user scratch list named "MCP Results" and dumps the enriched leads there. First use creates it; later calls reuse it. Pass an explicit lead_list_id to control where results go. (Company cohorts ignore lead_list_id — results return inline.)
data_points determine the enrichment type and cost: Email + Phone → full (11/lead), Phone alone → phone_only (10/lead), Email/other → partial (1/lead).
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 populated company record.
Bulk list
POST /enrichment/bulk
Async. Requires a quote_id. Enriches every lead in an existing list via waterfall enrichment. Cost is bounded by the quote's max.
First estimate 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 | phone-only |
quote_id | string | Required. From POST /credits/estimate |
Response
{
"workflow_id": "bulk-enrich-9a1b...",
"status": "pending",
"total_leads": 250,
"estimated_cost": 250,
"credits_reserved": 250,
"poll_url": "/api/v1/public/enrichment/status/bulk-enrich-9a1b...",
"timestamp_ms": 1717430400000
}# 1. estimate
QUOTE=$(curl -s -X POST https://api.cleanlist.ai/api/v1/public/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/v1/public/enrichment/bulk \
-H "Authorization: Bearer clapi_your_api_key" -H "Content-Type: application/json" \
-d "{\"list_id\":\"LIST_ID\",\"scope\":\"partial\",\"quote_id\":\"$QUOTE\"}"Status
GET /enrichment/status/{workflow_id}
Scope: enrich:read · Cost: free
Poll any async enrichment. Accepts a workflow_id (e.g. enrich-... or bulk-enrich-...) or an MCPTask cohort handle (cl-task_...) — Cleanlist resolves the cohort to its underlying 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 | Workflow state (pending, running, completed, failed, cancelled) |
progress | Percent complete (when known) |
total / processed / completed / failed | Cohort counters |
result | Inline enriched lead — populated only for terminal single-person runs; null for bulk |
credits_charged / credits_refunded | Settled billing after the run finishes |
refund_status | pending_review if a prepaid run failed and needs manual refund; otherwise not_applicable |
For terminal single-person runs, result carries the enriched lead inline:
{
"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",
"provider": "hunter"
}
}For polling patterns and chaining, see Polling & Async Results.