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.
| 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.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
| 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 |
phone_waterfall | enum | standard (default) or gb for the UK mobile waterfall; see Enrichment Types |
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 |
phone_waterfall | enum | Person cohorts only: standard (default) or gb; see Enrichment Types |
quote_id | string | Required. 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
| 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 |
phone_waterfall | enum | standard (default) or gb; see Enrichment Types |
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 CluGTM resolves to its workflow. A workflow from another workspace returns 404 not_found.
| Query param | Default | Notes |
|---|---|---|
include_results | false | Bulk runs: also return one page of per-contact results |
results_offset | 0 | First contact index of the page |
results_limit | 100 | Page 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
}| Field | Meaning |
|---|---|
status | Not 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 |
progress | Percent complete, when known |
total, processed, completed, failed | Cohort counters. processed is completed + failed |
opted_out | Bulk runs: how many of failed were refused because the person opted out of data enrichment (null when not counted) |
summary | Aggregate counters for the run, such as emails_found and phones_found |
result | The enriched lead, inline, for terminal single-person runs only; null for bulk |
results, results_next_offset | Bulk 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_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"
}
}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
}| Field | Meaning |
|---|---|
index | Position of the contact in the run |
status | pending, processing, completed or failed while known; unavailable once the record has expired |
failure_class | Why the contact failed, for example not_found |
lead | The 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.