New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
API Reference (v2)
Enrichment
🔌

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.

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 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/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.

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

FieldTypeNotes
lead_list_idstringRequired. Target list for the enriched lead
emailstringOne strong identifier...
linkedin_urlstring...accepts many URL forms (normalized on the server)
person_idstring...an existing prospect id
first_name + last_name + (company_name or domain)string...or a name + company combination
phonestringOptional known phone
company_name, domainstringOptional company context
enrichment_typeenumpartial, phone_only or full (preferred)
include_phonebooleanLegacy alternative to enrichment_type
quote_idstringOptional 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 starts queued.
  • Company cohorts are synchronous. The response carries each entity's record inline, and workflow_id is null.

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

FieldTypeNotes
task_idstringRequired. The cohort handle
data_pointsstring[]Required. Person cohorts: Email, Phone, LinkedIn. Company cohorts: Industry, Employees, Revenue, Funding, TechStack, Domain
entity_idsstring[]Optional subset of the cohort
lead_list_idstringWhere person results land; see below
quote_idstringRequired. 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

FieldTypeNotes
list_idstringRequired. The list to enrich
scopeenumpartial (default), full or phone-only
quote_idstringRequired. From POST /credits/estimate
lead_idsstring[]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
}
FieldMeaning
statusNot 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
progressPercent complete, when known
total, processed, completed, failedCohort counters
resultThe enriched lead, inline, for terminal single-person runs only; null for bulk
credits_charged, credits_refundedSettled billing after the run finishes
refund_statuspending_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.

Learn more