Audience: developers. The public API requires a Pro plan or above (self-serve) or an AppSumo Tier 5+ license. 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 — by cohort task_id (to save search results) or by lead_ids (already-materialized leads), 0.5 credits/lead on new rows — read leads with phone and full_name, update name/description/folder, delete a list, remove leads, or import a CSV with optional bulk enrichment. Lists carry an owner_name and a visibility of private / organization / shared.
Lead Lists
Lead lists are how you organize contacts in Cleanlist. The Public API 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) |
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 same-name list, no new list is created — the endpoint returns 200 with the existing list and reused: true instead of 201.
Request body
| Field | Type | Notes |
|---|---|---|
name | string | Required. 1–200 chars |
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
}List
GET /lead-lists
Paginates the lists you can access — owned, organization-wide, and shared.
| Query param | Default | Range |
|---|---|---|
limit | 50 | 1–100 |
cursor | — | opaque cursor from the previous page |
folder_id | — | 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": "Victor Paraschiv",
"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
}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 are updated — omitted fields are left unchanged. Returns the updated list object.
Request body
| Field | Type | Notes |
|---|---|---|
name | string | Optional. 1–200 chars |
description | string | Optional |
folder_id | string | Optional. UUID of a folder you own; null to remove 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
}Delete
DELETE /lead-lists/{list_id}
Scope: lists:write
Permanently deletes a list and removes all its lead memberships. The underlying lead records remain in your workspace if they belong to other lists. Returns 204 No Content on success.
curl -X DELETE https://api.cleanlist.ai/api/v2/lead-lists/9c1f... \
-H "Authorization: Bearer clapi_your_api_key"Deletion is permanent and irreversible. Protected system lists (such as "Extension Leads") cannot be deleted and return 403.
List leads
GET /lead-lists/{list_id}/leads
Paginates the leads in a list.
| Query param | Default | Range |
|---|---|---|
limit | 100 | 1–500 |
cursor | — | 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 no location) |
{
"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 re-listing every lead_id.
Add leads
POST /lead-lists/{list_id}/leads
The request body takes exactly one of two mutually-exclusive forms (sending both, or neither, is a 422). Filter-based add is intentionally rejected — translate a search into one of these forms first.
| Form | Body | Use it for |
|---|---|---|
| By cohort | { "task_id": "cl-task_...", "entity_ids": [...] } | Saving search results. task_id is the cohort handle from a search_people (or list-leads) response. entity_ids is an optional subset of that cohort (max 2000); omit it to save the whole cohort. |
| By lead id | { "lead_ids": ["...", "..."] } | Adding leads that already exist as rows — i.e. from a list your org owns, or from a list_leads_in_list response. 1–1000 UUIDs. |
Saving a search_people result? You must use the cohort form (task_id). Search rows carry internal search ids, not lead UUIDs, so they cannot be passed as lead_ids — that would 404. Pass the search's task_id instead (optionally narrowed by entity_ids); Cleanlist materializes the matching leads into the list. The lead_ids form is only for leads that are already materialized (an owned list or list_leads_in_list output).
Adding leads costs 0.5 credits per lead, charged at the moment leads are added to the list (not deferred to enrichment). Only newly-added rows are billed — re-adding a lead already in the list is free (skipped_duplicates). The settled amount is returned as credits_charged.
# 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 already-materialized leads by id (from an owned list / list_leads_in_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": 1.0 }Remove leads
DELETE /lead-lists/{list_id}/leads
Removes leads from a list by lead_ids (max 100). This removes membership only — the lead stays in your workspace and in any other lists. Unknown ids are silently 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 optionally dispatches bulk enrichment in the same call. Returns 201. Requires lists:write; if dispatch_enrichment is true, also requires enrich:write.
The raw CSV body is capped at ~150 KB / ~600 rows — past that the endpoint returns 413. Chunk larger files upstream. Import itself is free; the enrichment dispatch 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. Kick off bulk enrichment after import |
enrichment_type | enum | Required when dispatching: partial, phone_only, full |
quote_id | string | Optional signed quote (tool: "bulk_import_csv") for large dispatches |
idempotency_key | string | Optional. Replays the cached response for 24h (org-wide); no re-import, 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\"
}"After a dispatching import, poll the returned enrichment_status_url — see Enrichment status.