Skip to Content
New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
🔌

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

CluGTM 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.clugtm.com/billing”; the actual page is the Plans page at app.clugtm.com/plans .

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
phone_waterfallenumstandard (default) or gb for the UK mobile waterfall; see Enrichment Types
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
phone_waterfallenumPerson cohorts only: standard (default) or gb; see Enrichment Types
quote_idstringRequired. Signed quote from POST /credits/estimate

The “MCP Results” list. For person cohorts, if you omit lead_list_id, CluGTM 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
phone_waterfallenumstandard (default) or gb; see Enrichment Types
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 CluGTM resolves to its workflow. A workflow from another workspace returns 404 not_found.

Query paramDefaultNotes
include_resultsfalseBulk runs: also return one page of per-contact results
results_offset0First contact index of the page
results_limit100Page size, 1 to 500

Response

{ "workflow_id": "bulk-enrich-9a1b...", "status": "completed", "progress": 100, "total": 250, "processed": 250, "completed": 231, "failed": 19, "opted_out": 2, "enrichment_type": "partial", "lead_list_id": "LIST_ID", "summary": { "emails_found": 231, "phones_found": 0 }, "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. A run that was stopped ends cancelled while its run is cached, or canceled (one l) or terminated once the status comes from the workflow itself. Poll until you see a terminal value
progressPercent complete, when known
total, processed, completed, failedCohort counters. processed is completed + failed
opted_outBulk runs: how many of failed were refused because the person opted out of data enrichment (null when not counted)
summaryAggregate counters for the run, such as emails_found and phones_found
resultThe enriched lead, inline, for terminal single-person runs only; null for bulk
results, results_next_offsetBulk runs polled with include_results=true: one page of per-contact outcomes, and the results_offset for the next page (null at the end). Both null otherwise
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" } }

Per-contact results for bulk runs

Add include_results=true to see each contact of a bulk run, not just the counters. Page with results_offset and results_limit until results_next_offset is null. The example is shortened.

curl "https://api.cleanlist.ai/api/v2/enrichment/status/bulk-enrich-9a1b...?include_results=true&results_offset=0&results_limit=100" \ -H "Authorization: Bearer clapi_your_api_key"
{ "workflow_id": "bulk-enrich-9a1b...", "status": "completed", "results": [ { "index": 0, "status": "completed", "failure_class": null, "lead": { "lead_id": "lead_abc", "full_name": "Jane Doe", "email": "jane@acme.com", "linkedin_url": "https://www.linkedin.com/in/janedoe", "provider": "leadmagic" } }, { "index": 1, "status": "failed", "failure_class": "not_found", "lead": null } ], "results_next_offset": 100 }
FieldMeaning
indexPosition of the contact in the run
statuspending, processing, completed or failed while known; unavailable once the record has expired
failure_classWhy the contact failed, for example not_found
leadThe saved lead in the destination list, the same lead row GET /lead-lists/{id}/leads returns, with the same reveal rules. null when no lead was saved

Per-contact records are kept for 24 hours. After that, status reads unavailable; read the destination list with GET /lead-lists/{id}/leads instead. Single-person runs ignore include_results and return result.

For polling patterns and chaining, see Polling & Async Results. To be told when a bulk run finishes instead, subscribe to the enrichment.completed webhook event.

Learn more