Skip to Content
New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
Legacy API (v1)Enrichment
🔌

Audience: developers. API keys and the REST API are included on Pro and Enterprise (and legacy Pro and Scale plans) and on AppSumo Tiers 5 to 9. They are not available on Free, Starter, AppSumo Tiers 1 to 4 or during the Pro trial. Support agent: confirm the user’s plan includes API access before giving API advice.

TL;DR: POST /enrich/bulk takes 1 to 250 contacts and a required lead_list_id, and returns a workflow_id plus one task_id per contact. Poll GET /enrich/status with exactly one of workflow_id or task_id, or pass a webhook_url to get one POST with every result. Contacts already in that list are skipped. Your balance must cover the worst case before the run starts, but you’re charged only for data found.

Enrichment

The v1 API has two enrichment endpoints:

MethodPathPurpose
POST/api/v1/public/enrich/bulkStart an enrichment run for up to 250 contacts
GET/api/v1/public/enrich/statusPoll a run or a single contact

Each run goes through CluGTM’s waterfall, which tries providers in sequence until it finds reliable data.

Bulk enrichment

POST /api/v1/public/enrich/bulk

curl -X POST https://api.cleanlist.ai/api/v1/public/enrich/bulk \ -H "Authorization: Bearer clapi_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "enrichment_type": "partial", "webhook_url": "https://your-app.com/webhooks/cleanlist", "lead_list_id": "your_lead_list_id", "contacts": [ { "first_name": "John", "last_name": "Doe", "company_domain": "acme.com" }, { "linkedin_url": "https://www.linkedin.com/in/janedoe" }, { "first_name": "Sam", "last_name": "Lee", "company_name": "Globex" } ] }'

Request body

FieldTypeRequiredDescription
enrichment_typestringNo (default "partial")partial, phone_only or full. See Enrichment types
webhook_urlURLNoCluGTM POSTs every result here once the run finishes. See Webhooks
lead_list_idstringYesThe lead list the enriched contacts are saved to. It must be a list in your API key’s workspace that the key’s user can edit, and its per-list contact limit applies. Extension Leads can’t be used. Copy the id from the list’s URL in the app (app.cleanlist.ai/lists/<id>) or get it from GET /api/v2/lead-lists
contactsarrayYes1 to 250 contact objects

Contact object

Each contact must include either:

  • linkedin_url (gives the best match rate), or
  • first_name + last_name + (company_domain or company_name)
FieldTypeNotes
linkedin_urlstringLinkedIn profile URL. Enough on its own
first_namestringRequired when there is no LinkedIn URL
last_namestringRequired when there is no LinkedIn URL
company_domainstringFor example "acme.com". More accurate than the name
company_namestringUse when you don’t have the domain

Values are trimmed, and blank strings count as missing. A contact with only a company domain fails validation: to find people at a company, use the v2 Find People API first, then enrich the people it returns.

Duplicates

All contacts are saved to the list you pass as lead_list_id, and CluGTM skips duplicates before charging:

  • A contact whose LinkedIn URL is already in that list is skipped.
  • A contact sent without a LinkedIn URL is skipped when the same name and company is already in the list.
  • A contact repeated inside the same request is enriched once.

Skipped contacts are not enriched or charged, and the message in the response says how many were skipped. If every contact is a duplicate, the request fails with 400 “All N prospects are duplicates already in this list.”

Credit pre-check

Before the run starts, your workspace balance must cover the worst case: the number of contacts (after duplicates are removed) times the price of the enrichment_type (1 for partial, 10 for phone_only, 11 for full). Otherwise the request fails with 402, for example:

{ "error": { "code": "INSUFFICIENT_CREDITS", "message": "Insufficient credits. Required: 33.0 for 3 prospects, Available: 12. Please purchase more credits to continue." } }

You’re charged only for data found. See Credits.

Response

{ "workflow_id": "public_bulk_4d6f2c3a-1b2e-4a5b-9c8d-3e2f1a0b9d8e", "status": "processing", "message": "Queued 3 out of 3 prospects", "task_ids": [ "task_id_1", "task_id_2", "task_id_3" ], "total_contacts": 3 }
FieldDescription
workflow_idThe run. Use it to poll status and to look up webhook deliveries
statusInitial state, usually processing
messageSummary, including how many duplicates were skipped
task_idsOne per accepted contact, for per-contact polling
total_contactsNumber of contacts accepted into the run

Request errors

ConditionStatusMessage or code
Empty contacts400“At least one contact is required.”
More than 250 contacts400“Bulk enrichment supports up to 250 contacts per request.”
Every contact already in the list400“All N prospects are duplicates already in this list.”
No lead_list_id422validation_error
lead_list_id isn’t a UUID422validation_error
lead_list_id isn’t a list in your workspace404list_not_found
You can view the list but not edit it403list_not_editable
lead_list_id is your Extension Leads list403extension_leads_locked
The new contacts would take the list past its per-list contact limit403PERMISSION_DENIED, with details.error lead_cap_reached
A contact without enough identifiers, or an unknown enrichment_type422validation_error
Balance below the worst-case cost402INSUFFICIENT_CREDITS
Missing or invalid key401See Authentication
Rate limit exceeded429See Errors

validation_error, list_not_found, list_not_editable and extension_leads_locked come in the lowercase public envelope (error.code, error.problem, error.fix). The other errors, including the per-list limit, use the standard v1 envelope with an UPPERCASE error.code; for the limit, error.details also carries limit, current_count, remaining and attempted. See Errors.

Polling status

GET /api/v1/public/enrich/status

Poll by workflow_id (the whole run) or by task_id (one contact). Send exactly one: both or neither returns 400 “Provide exactly one of workflow_id or task_id.”

# Whole run curl "https://api.cleanlist.ai/api/v1/public/enrich/status?workflow_id=public_bulk_4d6f2c3a-..." \ -H "Authorization: Bearer clapi_your_api_key" # One contact curl "https://api.cleanlist.ai/api/v1/public/enrich/status?task_id=task_id_1" \ -H "Authorization: Bearer clapi_your_api_key"

Query parameters

ParameterDescription
workflow_idThe workflow_id from POST /enrich/bulk
task_idOne of the task_ids from the same response

Response by workflow_id

Run-level progress and counts:

{ "workflow": { "workflow_id": "public_bulk_4d6f2c3a-...", "status": "completed", "enrichment_type": "partial", "lead_list_id": "550e8400-e29b-41d4-a716-446655440000", "total_prospects": 3, "completed_prospects": 2, "failed_prospects": 1, "emails_found": 2, "phones_found": 0, "created_at": "2026-10-07T15:00:00Z", "modified_at": "2026-10-07T15:00:42Z" } }

Other fields may appear, such as opted_out_prospects (the failed contacts whose person opted out of data enrichment), and empty fields are left out. The run-level response doesn’t list each contact’s data: for that, poll each task_id, use a webhook, or read the list you passed as lead_list_id.

Response by task_id

One contact’s state and result:

{ "task": { "task_id": "task_id_1", "workflow_id": "public_bulk_4d6f2c3a-...", "status": "completed", "lead_id": "lead_uuid_1", "result": { "email": "john.doe@acme.com", "email_status": "verified", "phone": null, "provider": "wiza", "linkedin_url": "https://www.linkedin.com/in/johndoe" }, "credits_charged": 1 } }

A contact that found nothing has status: "failed" and an error message such as “No verified work email was found.” A failed task can also carry:

FieldDescription
failure_kindWhy it failed, for example miss (no data), error, rate_limited or person_opted_out
failure_classno_data, provider_error or timeout. Tells a provider outage apart from “no data found”
credits_refundedCredits given back for the contact, if any

These fields are left out when they don’t apply.

Status values

statusMeaning
processingThe run is still going
completedEvery contact finished and none failed
completed_with_errorsThe run finished; at least one contact found nothing or hit an error
failedThe run finished and no contact succeeded

Per-contact (task) statuses are processing, completed and failed.

How long status is kept

  • By task_id: 24 hours after the contact’s last update.
  • By workflow_id: about 7 days.

After that, the data lives on in the list you passed as lead_list_id. Read it in the app, or with GET /api/v2/lead-lists/{id}/leads (scope lists:read), using the lead_list_id from the run.

Polling or webhooks

You can use both at once.

ApproachBest for
Polling GET /enrich/statusScripts, testing, jobs you run end to end
Webhook (webhook_url)Production pipelines and serverless functions that shouldn’t keep a loop running

With webhook_url, CluGTM sends one POST with the summary and every contact’s result when the run finishes. See Webhooks.