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: The Cleanlist Public API is an agent- and MCP-friendly surface at api.cleanlist.ai/api/v2. Authenticate with a clapi_ Bearer token, each call requires an OAuth scope, and paid bulk operations use a signed quote_id from POST /credits/estimate. Every response uses a consistent envelope; every error uses a single error contract.
MCP API
This is the current API (v2), served at /api/v2. It's the full ~30-endpoint surface — search, enrichment, lead lists, smart agents, sync, and export — and is what all new integrations and the Cleanlist MCP should target. The older Legacy API (v1) at /api/v1/public is still supported but frozen. See API Versions & Migration for how the two relate and how to move over.
The Cleanlist MCP API lets you search for B2B contacts and companies, enrich them through waterfall enrichment, organize them into lead lists, run AI smart agents, and push results into your CRM or sequencer — all programmatically.
It was designed to be agent- and MCP-friendly: every response carries a consistent envelope so an LLM agent always knows where to find task_id, freshness, and follow-up hints, and paid operations are gated behind a signed estimate so an agent can't accidentally overspend.
Base URLs
| Surface | Base URL | Auth |
|---|---|---|
| Public API | https://api.cleanlist.ai/api/v2 | clapi_ key or Clerk JWT |
| API key management | https://api.cleanlist.ai/api/v1/api-keys | Clerk JWT only |
All requests must use HTTPS.
Authentication
Send your API key as a Bearer token:
Authorization: Bearer clapi_your_api_key_hereBeyond the token, every endpoint requires a specific OAuth scope (for example enrich:write to run an enrichment). The scopes granted to your credential are visible via GET /whoami. See Authentication for the full scope model and how to generate keys.
The response envelope
Every entity-returning endpoint wraps its payload in a consistent envelope. These fields are additive — they sit alongside the endpoint's own payload, and any may be null:
| Field | Type | Meaning |
|---|---|---|
task_id | string | A handle for chainable follow-ups (e.g. enrich a search cohort by task_id) |
timestamp_ms | integer | Server time the response was produced (freshness marker) |
agent_instructions | string | A per-call nudge for agents (credits remaining, truncation, warnings) |
usage | string | A "what to do next" hint, typically on empty/discovery responses |
message | string | A degraded-state explanation when something was skipped |
Clients that ignore unknown keys keep working as fields are added.
The error envelope
Every error under /api/v2/* follows one shape:
{
"error": {
"code": "insufficient_credits",
"problem": "This enrich_list call requires 110 credits; organization has 40.",
"cause": null,
"fix": "Top up at app.cleanlist.ai/billing, or reduce the scope of the call.",
"retryable": false,
"docs_url": "https://docs.cleanlist.ai/errors/insufficient-credits",
"request_id": "req_4f1c2a9b3d6e7f80",
"estimated_cost_credits": 110,
"available_credits": 40,
"shortfall_credits": 70
}
}Parse code to decide behavior; docs_url deep-links to the matching error page. See Errors & Rate Limits for the full code table.
Rate limits
| Limit | Window | Applies to |
|---|---|---|
| 60 requests / minute | per organization | all keys and members combined |
| 30 requests / minute | per API key | each individual clapi_ key |
When you exceed a limit you get a 429 with a Retry-After header and a rate_limited error body. See Errors & Rate Limits.
Credits & the quote model
Reads (search filters, list reads, status polls, exports, whoami, balance) are free — including search itself. The per-lead charge (0.5/lead) is applied when leads are saved to a list, and only on newly-added rows (re-adding a lead already in a list is free); enrichment is billed separately on top. Paid bulk operations — bulk enrichment, smart agents, CRM/sequencer sync, CSV import — require a signed quote:
- Call
POST /credits/estimatewith the operation shape. You get back aquote_id(HMAC-signed, 5-minute TTL) and theestimated_cost. - Pass that
quote_idto the paid endpoint. The server recomputes the cost and rejects withspend_cap_exceededif it would exceed the quote.
See Credits for balances, estimates, usage reporting, and the full cost table.
Quick start
A complete flow: generate a key, estimate a bulk enrichment, run it, then poll for completion.
# 1. Generate a key (Clerk JWT auth — usually done once in the portal)
curl -X POST https://api.cleanlist.ai/api/v1/api-keys \
-H "Authorization: Bearer <clerk_session_jwt>" \
-H "Content-Type: application/json" \
-d '{"name": "Production server"}'
# 2. Confirm identity, scopes, and tier
curl https://api.cleanlist.ai/api/v2/whoami \
-H "Authorization: Bearer clapi_your_api_key"
# 3. Estimate a bulk enrichment over an existing list
curl -X POST https://api.cleanlist.ai/api/v2/credits/estimate \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"tool": "enrich_list", "list_id": "LIST_ID", "scope": "partial"}'
# 4. Run it with the returned quote_id
curl -X POST https://api.cleanlist.ai/api/v2/enrichment/bulk \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{"list_id": "LIST_ID", "scope": "partial", "quote_id": "qte_v1_..."}'
# 5. Poll until terminal
curl https://api.cleanlist.ai/api/v2/enrichment/status/WORKFLOW_ID \
-H "Authorization: Bearer clapi_your_api_key"Endpoint index
All paths below are relative to https://api.cleanlist.ai/api/v2 unless noted. The Scope column lists the OAuth scope each endpoint requires.
Workspace & identity
| Method | Path | Scope |
|---|---|---|
GET | /whoami | none (any valid credential) |
GET | /credits/balance | credits:read |
POST | /credits/estimate | credits:read |
GET | /api-keys | admin:api_keys |
GET | /usage | admin:api_keys |
Search
| Method | Path | Scope |
|---|---|---|
GET | /search/people/filters | people:read |
POST | /search/people | people:read |
GET | /search/companies/filters | companies:read |
POST | /search/companies | companies:read |
POST | /search/companies/similar | companies:read |
Lead lists
| Method | Path | Scope |
|---|---|---|
POST | /lead-lists | lists:write |
GET | /lead-lists | lists:read |
GET | /lead-lists/{list_id} | lists:read |
PATCH | /lead-lists/{list_id} | lists:write |
DELETE | /lead-lists/{list_id} | lists:write |
GET | /lead-lists/{list_id}/leads | lists:read |
POST | /lead-lists/{list_id}/leads | lists:write |
DELETE | /lead-lists/{list_id}/leads | lists:write |
POST | /lead-lists/{list_id}/csv-import | lists:write (+ enrich:write if dispatching) |
Enrichment
| Method | Path | Scope |
|---|---|---|
POST | /enrichment/person | enrich:write |
POST | /enrichment/company | enrich:write |
POST | /enrichment/by-task | enrich:write |
POST | /enrichment/bulk | enrich:write |
GET | /enrichment/status/{workflow_id} | enrich:read |
Smart agents
| Method | Path | Scope |
|---|---|---|
POST | /smart-agents/run | smart_agents:write |
GET | /smart-agents | smart_agents:read |
GET | /smart-agents/{smart_agent_task_id} | smart_agents:read |
Smart agents run an AI agent over every lead in a list and write the result into a new column. Five agent_type values are supported:
agent_type | What it does | Needs prompt? | Cost / lead |
|---|---|---|---|
custom_ai | Runs your own prompt against each lead | Yes | 1 |
cold_intro_email | Drafts a personalized cold-intro email per lead | No | 3 |
title_normalizer | Normalizes job titles to a standard single-line form | No | 0.5 |
company_intel | Summarizes key company intelligence in 3–5 bullets | No | 2 |
pain_point_hypothesis | Hypothesizes 3–5 likely business pain points per lead | No | 2 |
POST /smart-agents/run takes list_id, agent_type (default custom_ai), column_name, an optional prompt (required for custom_ai; ignored by the presets), lead_scope (subset — up to max_rows, default 100 — or all), overwrite, and a signed quote_id. Running over a large list with lead_scope: "all" may return 400 approval_required — re-issue with a smaller scope or explicit approval. The response returns a smart_agent_task_id to poll via GET /smart-agents/{smart_agent_task_id}, which reports status, progress, total, succeeded, failed, and per-lead results ({lead_id, value, error}). See Polling for the async pattern and Credits for pricing.
Sync & export
| Method | Path | Scope |
|---|---|---|
POST | /sync/crm | sync:write |
POST | /sync/sequencer | sync:write |
POST | /export/csv/signed-url | export:read |
GET | /export/json | export:read |
API key management (Clerk JWT)
| Method | Path |
|---|---|
POST | /api/v1/api-keys |
GET | /api/v1/api-keys |
POST | /api/v1/api-keys/{key_id}/revoke |
POST | /api/v1/api-keys/revoke-all |
GET | /api/v1/api-keys/{key_id}/requests |
whoami
The fastest way to confirm a credential works and see what it can do:
curl https://api.cleanlist.ai/api/v2/whoami \
-H "Authorization: Bearer clapi_your_api_key"{
"user_email": "v***@cleanlist.ai",
"user_id": "user_2abc...",
"user_name": "Victor Paraschiv",
"organization_id": "org_2xyz...",
"organization_name": "Cleanlist",
"auth_type": "api_key",
"scopes": ["people:read", "lists:read", "lists:write", "enrich:read", "enrich:write", "credits:read"],
"tier": "pro",
"appsumo_tier": null,
"features": ["api_access", "data_source_csv_upload"],
"timestamp_ms": 1717430400000
}For api_key auth, user_email is masked so a leaked key can't reveal the creator's full address.