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: POST /export/csv/signed-url builds a CSV of a list and returns a download link valid for 24 hours; a list that takes longer than 20 seconds answers 202 with a job_id to call back with. GET /export/json returns leads inline, up to 500 per call, with cursor paging. Both need export:read and are free.
Export
Two ways to get a lead list out of CluGTM through API v2. To export from the app instead, see Exporting data.
| Endpoint | Method | Scope | Cost |
|---|---|---|---|
| CSV (signed URL) | POST /export/csv/signed-url | export:read | Free |
| JSON | GET /export/json | export:read | Free |
Columns
Both endpoints use the same columns. Without columns, you get the default set, in this order:
| Column | Contents |
|---|---|
lead_id | Lead id |
first_name, last_name | Name |
email | Primary email (empty until enriched) |
title | Job title |
company | Company name |
domain | Company domain |
linkedin_url | LinkedIn profile URL |
phone | First phone number (empty until phone-enriched) |
location | City and country |
Pass columns to pick a subset and set the order. Stick to these names; other names may come back empty.
CSV export
POST /export/csv/signed-url
Scope: export:read · Cost: free
Starts an export of the whole list (import rows not yet enriched come first) and waits up to 20 seconds for it. If the file is ready, the response has status: "completed" and a signed link. A bigger list answers HTTP 202 with status: "running", a job_id and download_url: null: call again with the same list_id and the job_id to get the link. A call with job_id starts no new export, and it waits up to 20 seconds again.
Request body
| Field | Type | Notes |
|---|---|---|
list_id | string | Required. A list you can read |
columns | string[] | Optional ordered subset of the columns above |
include_smart_agents | boolean | Default true: adds one column per skill or smart-agent result saved on the list (headed by its display name) after your columns. false exports only the base columns |
job_id | string | The job_id of an export that answered status: "running". Send it with the same list_id to get the link |
curl -X POST https://api.cleanlist.ai/api/v2/export/csv/signed-url \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"list_id": "LIST_ID", "columns": ["first_name", "last_name", "email", "company"]}'Response
{
"list_id": "LIST_ID",
"job_id": "JOB_ID",
"status": "completed",
"download_url": "https://...",
"expires_at": "2026-10-08T21:20:00+00:00",
"row_count": 1284,
"file_size_bytes": 402118,
"skipped_count": 0
}A list that isn’t ready within 20 seconds answers 202:
{
"list_id": "LIST_ID",
"job_id": "JOB_ID",
"status": "running",
"download_url": null,
"expires_at": null,
"row_count": 48210,
"file_size_bytes": null,
"skipped_count": 0
}curl -X POST https://api.cleanlist.ai/api/v2/export/csv/signed-url \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"list_id": "LIST_ID", "job_id": "JOB_ID"}'| Field | Meaning |
|---|---|
status | completed with a download_url, or running (HTTP 202): call again with job_id |
job_id | The export job. Send it back to poll a running export |
download_url | Signed link to the CSV file (null while running). Anyone with the link can download it until it expires, so treat it like a secret |
expires_at | When the link stops working, 24 hours after it was made |
row_count | Data rows in the file (not counting the header); while running, the rows being exported |
file_size_bytes | File size |
skipped_count | Rows that changed while the list was exporting (deleted, or merged into another row) and aren’t in the file. row_count + skipped_count is the list’s row count when the export started |
To stop spreadsheet apps from running formulas, any cell that starts with =, +, - or @ is written with a leading apostrophe (').
With an SDK: export.export_csv(...) and export.export_json(...) (exportCsv and exportJson in TS and Java). See the SDKs.
JSON export
GET /export/json
Scope: export:read · Cost: free
Returns a page of the list’s leads inline, using the same columns.
| Query param | Default | Notes |
|---|---|---|
list_id | none | Required |
limit | 100 | 1 to 500 rows per call |
cursor | none | The cursor from the previous response |
columns | default set | Repeat the parameter for each column, for example columns=email&columns=company |
curl "https://api.cleanlist.ai/api/v2/export/json?list_id=LIST_ID&limit=500" \
-H "Authorization: Bearer clapi_your_api_key"{
"list_id": "LIST_ID",
"leads": [
{ "lead_id": "lead_5f9d2b", "first_name": "Ada", "last_name": "Lovelace", "email": "ada@analytical.io", "company": "Analytical", "domain": "analytical.io" }
],
"total": 1284,
"cursor": "500",
"skipped": 0,
"timestamp_ms": 1751404800000
}Pass cursor back to get the next page; it’s null on the last page. total is the number of rows in the whole list, import rows not yet enriched included. skipped counts rows of this page’s place in the order that left the list (deleted or moved) after paging started, so the page holds fewer rows than it covers. A malformed cursor returns 400 invalid_cursor instead of starting over, and a cursor unused for 15 minutes returns 409 cursor_expired: start again without a cursor.
Errors
| Code | HTTP | Cause |
|---|---|---|
insufficient_scope | 403 | The credential lacks export:read |
list_not_found | 404 | Unknown list, or a private list you can’t access |
invalid_cursor | 400 | The JSON cursor isn’t one the API returned |
cursor_expired | 409 | The JSON cursor’s ordering expired (15 minutes unused). Start again without a cursor |
not_found | 404 | No CSV export has that job_id for this list. Start a new export without job_id |
internal_error | 502 | The export couldn’t be started, read or built; retry |