New: API Reference docs are live — integrate Cleanlist enrichment into your apps. View API docs →
MCP API
Enrichment

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.

EndpointMethodSync?Scope
Single personPOST /enrichment/personasyncenrich:write
Single companyPOST /enrichment/companysyncenrich:write
By task (cohort)POST /enrichment/by-taskperson async / company syncenrich:write
Bulk listPOST /enrichment/bulkasyncenrich:write
StatusGET /enrichment/status/{workflow_id}syncenrich: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/status reports the settled credits_charged and credits_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

FieldTypeNotes
lead_list_idstringRequired. Target list for the enriched lead
emailstringOne strong identifier…
linkedin_urlstring…accepts many forms (canonicalized server-side)
person_idstring…an existing prospect id
first_name + last_name + (company_name or domain)string…or a name + company combo
phonestringOptional known phone
company_name, domainstringOptional company context
enrichment_typeenumpartial | phone_only | full (preferred)
include_phonebooleanLegacy alternative to enrichment_type
quote_idstringOptional 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 all queued.
  • Company tasks → synchronous. Returns each entity's looked-up record inline; workflow_id is null.

No quote is required — the cohort is already scope-bound by the task's entity_ids.

Request body

FieldTypeNotes
task_idstringRequired. The cohort handle from a prior search/list read
data_pointsstring[]Required. For person tasks: Email, Phone, LinkedIn. For company tasks: Industry, Employees, Revenue, Funding, TechStack, Domain
entity_idsstring[]Optional subset of the task's cohort (must be a strict subset)
lead_list_idstringWhere 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 + Phonefull (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

FieldTypeNotes
list_idstringRequired. The list to enrich
scopeenumpartial (default) | full | phone-only
quote_idstringRequired. 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
}
FieldMeaning
statusWorkflow state (pending, running, completed, failed, cancelled)
progressPercent complete (when known)
total / processed / completed / failedCohort counters
resultInline enriched lead — populated only for terminal single-person runs; null for bulk
credits_charged / credits_refundedSettled billing after the run finishes
refund_statuspending_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.

Learn more