TL;DR: The Cleanlist Public API is an agent- and MCP-friendly surface at api.cleanlist.ai/api/v1/public. 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
Two public APIs, same base path. Cleanlist exposes two APIs under /api/v1/public: this MCP API (the newer, expanded ~30-endpoint surface documented here, built for agents and integrations) and the older legacy Public API (5 endpoints — validate-key, enrich/bulk, enrich/status, webhooks/deliveries, credits/balance — including webhook delivery). Both are live; use whichever fits your integration.
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/v1/public | 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/v1/public/* 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. Search defers a small per-lead charge to enrichment/save time. 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/v1/public/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/v1/public/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/v1/public/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/v1/public/enrichment/status/WORKFLOW_ID \
-H "Authorization: Bearer clapi_your_api_key"Endpoint index
All paths below are relative to https://api.cleanlist.ai/api/v1/public 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 |
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 |
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/v1/public/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",
"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.