New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
API Reference (v2)
Lead Lists
🔌

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.

EndpointMethodScope
CreatePOST /lead-listslists:write
ListGET /lead-listslists:read
Get oneGET /lead-lists/{list_id}lists:read
UpdatePATCH /lead-lists/{list_id}lists:write
DeleteDELETE /lead-lists/{list_id}lists:write
List leadsGET /lead-lists/{list_id}/leadslists:read
Add leadsPOST /lead-lists/{list_id}/leadslists:write
Remove leadsDELETE /lead-lists/{list_id}/leadslists:write
CSV importPOST /lead-lists/{list_id}/csv-importlists:write (+ enrich:write if dispatching)
Find peoplePOST /lead-lists/{list_id}/find-peoplelists: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

FieldTypeNotes
namestringRequired. 1 to 200 characters
descriptionstringOptional
folder_idstringOptional 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 paramDefaultRange
limit501 to 100
cursornoneopaque cursor from the previous page
folder_idnonefilter 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

FieldTypeNotes
namestringOptional. 1 to 200 characters
descriptionstringOptional
folder_idstringOptional. 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 paramDefaultRange
limit1001 to 500
cursornoneopaque cursor

Lead row schema

FieldTypeNotes
lead_idstringStable lead id
first_namestring
last_namestring
full_namestringPre-composed display name
emailstringPrimary email (null until enriched)
phonestringPrimary verified phone (null until phone-enriched)
titlestringJob title
companystringCompany name
locationstringComposed 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.

FormBodyUse 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 }
FieldMeaning
addedLeads newly added to the list
skipped_duplicatesLeads already in the list, skipped
createdCohort saves only: new lead records created (versus reusing an existing lead for the same person)
credits_chargedAlways 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

FieldTypeNotes
csv_content_base64stringRequired. Base64-encoded UTF-8 CSV
column_mappingobjectRequired. { 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_enrichmentbooleanDefault true. Start bulk enrichment after import
enrichment_typeenumRequired when dispatching: partial, phone_only or full
quote_idstringOptional signed quote (tool: "bulk_import_csv")
idempotency_keystringOptional. 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.

Learn more