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: Full lead-list CRUD. Create a list, add leads for free (by cohort task_id to save search results, or by lead_ids for existing leads), read leads with phone and full_name, update name, description or folder, delete a list, remove leads, import a CSV with optional bulk enrichment, or find people at the list's companies.
Lead Lists
Lead lists are how you organize contacts in Cleanlist, the same Lead Lists you see in the app. API v2 exposes full CRUD plus CSV import.
| Endpoint | Method | Scope |
|---|---|---|
| Create | POST /lead-lists | lists:write |
| List | GET /lead-lists | lists:read |
| Get one | GET /lead-lists/{list_id} | lists:read |
| Update | PATCH /lead-lists/{list_id} | lists:write |
| Delete | DELETE /lead-lists/{list_id} | lists:write |
| List leads | GET /lead-lists/{list_id}/leads | lists:read |
| Add leads | POST /lead-lists/{list_id}/leads | lists:write |
| Remove leads | DELETE /lead-lists/{list_id}/leads | lists:write |
| CSV import | POST /lead-lists/{list_id}/csv-import | lists:write (+ enrich:write if dispatching) |
| Find people | POST /lead-lists/{list_id}/find-people | lists:write |
Create
POST /lead-lists
Creates a private list in your workspace. Returns 201.
Create is idempotent on name (case-insensitive) for the same owner: if you already own a list with that name, no new list is created and the endpoint returns 200 with the existing list and reused: true.
Request body
| Field | Type | Notes |
|---|---|---|
name | string | Required. 1 to 200 characters |
description | string | Optional |
folder_id | string | Optional folder UUID you own |
curl -X POST https://api.cleanlist.ai/api/v2/lead-lists \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Q3 Outbound: Fintech VPs"}'{
"list_id": "9c1f...",
"name": "Q3 Outbound: Fintech VPs",
"description": null,
"lead_count": 0,
"enriched_count": 0,
"folder_id": null,
"created_at": "2026-06-03T10:00:00Z",
"timestamp_ms": 1717430400000
}If your plan's list limit is reached, create returns 403 list_cap_reached; delete a list or upgrade, then retry.
List
GET /lead-lists
Pages through the lists you can access: owned, organization-wide and shared.
| Query param | Default | Range |
|---|---|---|
limit | 50 | 1 to 100 |
cursor | none | opaque cursor from the previous page |
folder_id | none | filter to one folder |
{
"lists": [
{
"list_id": "9c1f...",
"name": "Q3 Outbound: Fintech VPs",
"lead_count": 128,
"folder_id": null,
"created_at": "2026-06-03T10:00:00Z",
"owner_name": "Your Name",
"visibility": "private"
}
],
"total": 14,
"cursor": "50",
"timestamp_ms": 1717430400000
}visibility is one of private, organization or shared. owner_name is the list creator's display name (may be null).
Get one
GET /lead-lists/{list_id}
{
"list_id": "9c1f...",
"name": "Q3 Outbound: Fintech VPs",
"description": null,
"lead_count": 128,
"enriched_count": 96,
"folder_id": null,
"created_at": "2026-06-03T10:00:00Z",
"timestamp_ms": 1717430400000
}A list that doesn't exist, or that belongs to another workspace, returns 404 list_not_found.
Update
PATCH /lead-lists/{list_id}
Scope: lists:write
Rename a list, update its description, or move it to a different folder. Only the fields you send change. Returns the updated list object.
Request body
| Field | Type | Notes |
|---|---|---|
name | string | Optional. 1 to 200 characters |
description | string | Optional |
folder_id | string | Optional. UUID of a folder you own; null to remove it from any folder |
curl -X PATCH https://api.cleanlist.ai/api/v2/lead-lists/9c1f... \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Q3 Outbound: Fintech VPs (revised)", "folder_id": "folder_abc"}'{
"list_id": "9c1f...",
"name": "Q3 Outbound: Fintech VPs (revised)",
"description": null,
"lead_count": 128,
"enriched_count": 96,
"folder_id": "folder_abc",
"created_at": "2026-06-03T10:00:00Z",
"timestamp_ms": 1717430400000
}A list shared with you as view-only returns 403 list_not_editable on any write.
Delete
DELETE /lead-lists/{list_id}
Scope: lists:write
Permanently deletes a list and removes all its lead memberships. Lead records that also belong to other lists stay in those lists. Returns HTTP 200 with a small confirmation body:
curl -X DELETE https://api.cleanlist.ai/api/v2/lead-lists/9c1f... \
-H "Authorization: Bearer clapi_your_api_key"{ "list_id": "9c1f...", "deleted": true, "timestamp_ms": 1717430400000 }Deletion is permanent. Protected system lists (such as "Extension Leads") can't be renamed or deleted and return 403 list_protected.
List leads
GET /lead-lists/{list_id}/leads
Pages through the leads in a list.
| Query param | Default | Range |
|---|---|---|
limit | 100 | 1 to 500 |
cursor | none | opaque cursor |
Lead row schema
| Field | Type | Notes |
|---|---|---|
lead_id | string | Stable lead id |
first_name | string | |
last_name | string | |
full_name | string | Pre-composed display name |
email | string | Primary email (null until enriched) |
phone | string | Primary verified phone (null until phone-enriched) |
title | string | Job title |
company | string | Company name |
location | string | Composed City, State, Country (null when unknown) |
{
"list_id": "9c1f...",
"leads": [
{
"lead_id": "lead_abc",
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"email": "jane@acme.com",
"phone": "+14155550123",
"title": "VP Sales",
"company": "Acme",
"location": "San Francisco, California, United States"
}
],
"total": 128,
"cursor": "100",
"task_id": "cl-task_def...",
"timestamp_ms": 1717430400000
}The response carries a task_id, a cohort handle for these leads. Pass it to POST /enrichment/by-task to enrich the page without listing every lead_id. For more columns (including smart-agent results), use GET /export/json.
Add leads
POST /lead-lists/{list_id}/leads
The body takes exactly one of two forms (sending both, or neither, returns 422 validation_error). Adding by filter isn't supported; run a search first and save its cohort.
| Form | Body | Use it for |
|---|---|---|
| By cohort | { "task_id": "cl-task_...", "entity_ids": [...] } | Saving search results. task_id is the cohort handle from a POST /people/find (or list-leads) response. entity_ids is an optional subset of that cohort (up to 2000); omit it to save the whole cohort |
| By lead id | { "lead_ids": ["...", "..."] } | Adding leads that already exist in your workspace, for example from another list. 1 to 1000 ids |
Saving a search result? Use the cohort form (task_id). Search rows carry search-scoped ids, not lead ids, so they can't be passed as lead_ids (that returns 404). Pass the search's task_id, optionally narrowed by entity_ids, and Cleanlist creates the matching leads in the list.
Adding leads is free: there is no per-lead import charge, and credits_charged is always 0. Enrichment is billed separately.
# Save a search result: pass the cohort handle (task_id), not lead_ids
curl -X POST https://api.cleanlist.ai/api/v2/lead-lists/9c1f.../leads \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"task_id": "cl-task_abc123"}'# Add existing leads by id (for example, from another list)
curl -X POST https://api.cleanlist.ai/api/v2/lead-lists/9c1f.../leads \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"lead_ids": ["lead_abc", "lead_def"]}'{ "list_id": "9c1f...", "added": 2, "skipped_duplicates": 0, "credits_charged": 0 }| Field | Meaning |
|---|---|
added | Leads newly added to the list |
skipped_duplicates | Leads already in the list, skipped |
created | Cohort saves only: new lead records created (versus reusing an existing lead for the same person) |
credits_charged | Always 0 |
If the save would push the list past your plan's per-list contact limit, you get 403 lead_cap_reached. If a saved profile conflicts with the search result (different company or role), you get 409 search_result_conflict.
Remove leads
DELETE /lead-lists/{list_id}/leads
Removes leads from a list by lead_ids (up to 100). This removes membership only: the lead stays in your workspace and in any other lists. Unknown ids are ignored.
curl -X DELETE https://api.cleanlist.ai/api/v2/lead-lists/9c1f.../leads \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"lead_ids": ["lead_abc"]}'{ "list_id": "9c1f...", "removed": 1 }CSV import
POST /lead-lists/{list_id}/csv-import
Imports a CSV into a list and can start bulk enrichment in the same call. Returns 201. Needs lists:write; if dispatch_enrichment is true, also enrich:write.
The raw CSV is capped at about 150 KB or 600 rows; larger bodies return 413. Split bigger files before sending. Import itself is free; enrichment costs per lead (see Enrichment Types).
Request body
| Field | Type | Notes |
|---|---|---|
csv_content_base64 | string | Required. Base64-encoded UTF-8 CSV |
column_mapping | object | Required. { canonical_field: csv_header }. Canonical fields: first_name, last_name, full_name, email, phone, mobile_phone, work_phone, linkedin_url, company_name, company_domain, job_title, city, state, country |
dispatch_enrichment | boolean | Default true. Start bulk enrichment after import |
enrichment_type | enum | Required when dispatching: partial, phone_only or full |
quote_id | string | Optional signed quote (tool: "bulk_import_csv") |
idempotency_key | string | Optional. Replays the cached response for 24 hours (organization-wide), with no re-import and no re-charge |
Response
{
"list_id": "9c1f...",
"rows_parsed": 500,
"rows_imported": 488,
"rows_skipped": 12,
"skip_reasons": { "missing_required": 8, "duplicate_email": 4 },
"sample_skip_rows": [3, 17, 42],
"workflow_id": "bulk-enrich-9a1b...",
"enrichment_status_url": "/api/v2/enrichment/status/bulk-enrich-9a1b...",
"idempotency_replayed": false,
"timestamp_ms": 1717430400000
}B64=$(base64 -i leads.csv)
curl -X POST https://api.cleanlist.ai/api/v2/lead-lists/9c1f.../csv-import \
-H "Authorization: Bearer clapi_your_api_key" -H "Content-Type: application/json" \
-d "{
\"csv_content_base64\": \"$B64\",
\"column_mapping\": {\"first_name\": \"First\", \"last_name\": \"Last\", \"company_domain\": \"Domain\"},
\"dispatch_enrichment\": true,
\"enrichment_type\": \"partial\"
}"With an SDK: lead_lists.csv_import(...) (csvImport in TS and Java). See the SDKs.
After a dispatching import, poll the returned enrichment_status_url; see Enrichment status. If CSV import is switched off for maintenance, the endpoint returns 503 not_enabled.
Find people at a list's companies
POST /lead-lists/{list_id}/find-people starts an async job that finds people (by title, seniority and location) at companies already in the list and imports them into it. Discovery and import are free. Poll GET /lead-lists/{list_id}/find-people/status/{workflow_id}. Full reference: People Finder API.