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: Quote with POST /credits/estimate (tool: "run_agent"), start a run with POST /agents/qualification/run or POST /agents/research/run, poll GET /agents/runs/{run_id}, then read GET /agents/runs/{run_id}/results. Up to 500 leads per run. The price per lead is your workspace's price for the skill, listed by GET /agents.
Agents API (Skills)
The Agents API runs the Research and Qualification skills over the leads in a lead list, the same skills as on the Skills tab of a list's Actions drawer. Results land in a column of the list and are readable here. These are not the automations on the Agents page, and to run your own per-lead prompt, use Smart Agents.
| Endpoint | Method | Scope | Cost |
|---|---|---|---|
| Run Qualification | POST /agents/qualification/run | smart_agents:write | Per lead, from the quote |
| Run Research | POST /agents/research/run | smart_agents:write | Per lead and research_scope, from the quote |
| Run status | GET /agents/runs/{run_id} | smart_agents:read | Free |
| Run results | GET /agents/runs/{run_id}/results | smart_agents:read | Free |
| List agents | GET /agents | smart_agents:read | Free |
| Create custom agent | POST /agents/custom | smart_agents:write | Free |
| Update custom agent | PATCH /agents/custom/{agent_id} | smart_agents:write | Free |
| Agent profile | GET /agents/profile | smart_agents:read | Free |
These endpoints are live on /api/v2 but aren't in the served OpenAPI spec (/openapi-public-v2.json) yet, so the SDKs don't wrap them. Call them over HTTP as shown here. They aren't available to AppSumo-only workspaces (a paid Cleanlist plan on top lifts this), which get 403 feature_gated.
The skills
| Agent | What each lead gets | Price |
|---|---|---|
qualification | A 0 to 100 fit_score, a verdict (qualified, disqualified or needs_review) with reasons, a CRM check (already in your CRM or not) and any disqualification flags | credits_per_lead in GET /agents |
research | Live web research on the company, the contact or both, set by research_scope | credits_per_lead_by_scope in GET /agents |
Both skills score and write against your workspace's agent profile: your ICP, personas, value props, competitors and exclusions. If verdicts look wrong, read the profile first.
Pricing
Each skill is priced per lead for your workspace, to a tenth of a credit, and the number in GET /agents is the number the run charges. Research has a different price for each research_scope. The default prices are in Skills; always read yours from GET /agents or the quote.
A run reserves the per-lead price times the leads, rounded up once to a whole credit (credits_reserved), and is charged the exact total, so the charge can be a fraction of a credit below the hold. The rest of the hold comes back when the run settles.
Run a skill
POST /agents/qualification/run
POST /agents/research/run
Scope: smart_agents:write
Both routes take the same body; Research adds research_scope.
Request body
| Field | Type | Notes |
|---|---|---|
list_id | string | Required. A list you can edit. A list you can't access, or can only view, returns 404 list_not_found |
lead_ids | string[] | 1 to 500 lead ids. Ids that aren't in the list are dropped |
task_id | string | A cohort handle from a search or list read. Only leads that are in this list run; more than 500 of them returns 400 validation_error |
scope | "all" | Every lead in the list. Over 500 leads returns 400 approval_required |
research_scope | enum | Research only: company, contact or both (default both). Sets depth and price |
quote_id | string | Required. From POST /credits/estimate; see below |
idempotency_key | string | Optional, 1 to 128 characters. A retry with the same key and the same inputs returns the original run instead of starting and billing a second one; the same key with different inputs returns 409 idempotency_conflict |
Send exactly one of lead_ids, task_id or scope: "all"; none or more than one returns 422. scope picks leads; research_scope picks research depth.
Quote first
Estimate with tool: "run_agent", the list_id, and agent_type set to qualification or research. For Research, also send scope set to the research_scope you will run, even when that is the default both: the quote is signed for that depth, and a quote for another depth returns quote_mismatch.
The quote prices the whole list, whatever you select, so a run over a subset costs less than the quote. If the list grows after the quote so the run would reserve more than the quote, you get 400 spend_cap_exceeded; quote again. Quotes last 5 minutes and are single use.
# 1. Quote (Research over company and contact)
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": "run_agent",
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"agent_type": "research",
"scope": "both"
}'
# 2. Run with the returned quote_id
curl -X POST https://api.cleanlist.ai/api/v2/agents/research/run \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"scope": "all",
"research_scope": "both",
"quote_id": "qte_v1_...",
"idempotency_key": "research-2026-11-10-01"
}'For Qualification, quote with "agent_type": "qualification" and no scope, then call POST /agents/qualification/run without research_scope:
{
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"lead_ids": ["b2d4f6a8-1c3e-4d5f-8a9b-0c1d2e3f4a5b"],
"quote_id": "qte_v1_..."
}Response
{
"run_id": "smart-column-3f9c8e2b-7d14-4f6a-9c0e-1b2d3f4a5b6c",
"agent": "qualification",
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"status": "pending",
"lead_count": 120,
"credits_reserved": 12,
"credits_per_lead": 0.1,
"status_url": "/api/v2/agents/runs/smart-column-3f9c8e2b-7d14-4f6a-9c0e-1b2d3f4a5b6c",
"timestamp_ms": 1751404800000
}| Field | Meaning |
|---|---|
run_id | The run handle. Poll it at status_url |
lead_count | Leads actually scheduled, after dropping ids outside the list |
credits_reserved | Credits held: credits_per_lead × lead_count, rounded up to a whole credit. A same-key retry reports the hold its first attempt opened |
credits_per_lead | The per-lead price the hold used |
The run starts in the background and the response returns at once with status: "pending". Dispatch is capped at 20 runs a minute per workspace (429 rate_limited).
Poll a run
GET /agents/runs/{run_id}
Scope: smart_agents:read · Cost: free
{
"run_id": "smart-column-3f9c8e2b-7d14-4f6a-9c0e-1b2d3f4a5b6c",
"agent": "qualification",
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"status": "processing",
"progress": 40,
"total_leads": 120,
"processed_leads": 48,
"failed_leads": 1,
"started_at": "2026-11-10T15:02:11",
"completed_at": null,
"results_url": "/api/v2/agents/runs/smart-column-3f9c8e2b-7d14-4f6a-9c0e-1b2d3f4a5b6c/results",
"timestamp_ms": 1751404800000
}status is pending or processing while the run works, and completed, failed or partial when it stops. Poll every few seconds until it stops; see Polling & Async. An unknown run id and one from another workspace both return 404 run_not_found.
Read results
GET /agents/runs/{run_id}/results
Scope: smart_agents:read · Cost: free
| Query param | Default | Notes |
|---|---|---|
min_fit_score | none | 0 to 100. Keeps rows scoring at least this; rows with no score are dropped |
verdict | none | qualified, disqualified or needs_review |
sort | fit_score_desc | Or fit_score_asc. Unscored rows always sort last |
limit | 25 | 1 to 100 |
cursor | none | From the previous page. A malformed cursor returns 400 invalid_cursor |
{
"run_id": "smart-column-3f9c8e2b-7d14-4f6a-9c0e-1b2d3f4a5b6c",
"agent": "qualification",
"list_id": "9c1f7e2a-4b6d-4f21-9a3e-2b7c8d1e5f04",
"status": "completed",
"total_rows": 120,
"returned": 25,
"counts_by_verdict": { "qualified": 41, "needs_review": 22, "disqualified": 56, "unscored": 1 },
"results": [
{
"lead_id": "b2d4f6a8-1c3e-4d5f-8a9b-0c1d2e3f4a5b",
"full_name": "Dana Reyes",
"title": "VP of Revenue Operations",
"company": "Acme Analytics",
"fit_score": 82,
"verdict": "qualified",
"verdict_reasons": ["RevOps owner at a 300-person SaaS company"],
"crm_status": { "is_existing": false, "matched_record": null },
"disqualification_flags": [],
"analysis": "Acme raised a $40M Series B in March…",
"analysis_truncated": true,
"structured_failed": false,
"error": null
}
],
"cursor": "25",
"timestamp_ms": 1751404800000
}| Field | Meaning |
|---|---|
total_rows | Rows from this run, before min_fit_score and verdict filter them |
counts_by_verdict | Verdict counts over the whole run, never narrowed by the filters. unscored counts rows with no verdict yet |
fit_score | 0 to 100, or null when the row isn't scored yet or the skill could only return prose (structured_failed: true) |
crm_status | The CRM check: is_existing and matched_record, or an error of no_crm_connected or lookup_timeout |
analysis | The skill's write-up in Markdown, cut to 600 characters (analysis_truncated says when). The full text is in the list column and in exports |
error | Why this lead failed, if it did |
Rows appear as leads finish, so a running job returns a partial set. Rows belong to the run that wrote them: if a later run of the skill rewrites a lead, that lead moves to the later run's results. Research rows use the same shape: read analysis; fit_score and verdict stay null.
List agents
GET /agents
Scope: smart_agents:read · Cost: free
Lists the skills your workspace can run, with your prices, plus every custom agent saved in the workspace.
{
"agents": [
{
"agent": "qualification",
"name": "Qualification",
"kind": "built_in",
"description": "Score each lead against your org ICP and check whether they are already in your CRM. Returns a fit score, a verdict, reasons, and disqualification flags.",
"credits_per_lead": 0.1,
"run_endpoint": "/agents/qualification/run"
},
{
"agent": "research",
"name": "Research",
"kind": "built_in",
"description": "Deep-research a lead's company and/or contact. `research_scope` controls both depth and price.",
"credits_per_lead_by_scope": { "company": 2.9, "contact": 1.4, "both": 3.0 },
"run_endpoint": "/agents/research/run"
},
{
"agent": "custom",
"name": "Series B fit",
"kind": "custom",
"description": "Scores against our Series B ICP",
"agent_id": "7d2e9b41-...",
"credits_per_lead": 0.2,
"run_endpoint": null
}
],
"total": 3,
"timestamp_ms": 1751404800000
}The prices above are examples; yours come back in the response. Custom agents have run_endpoint: null: they run from the app (or as a smart agent with your prompt), not from this API.
Custom agents
A custom agent is a saved, reusable prompt for the workspace, the same as a saved prompt on the Skills tab. Only org admins can create or edit one; anyone else gets 403 forbidden. There's no delete on the API.
POST /agents/custom
Scope: smart_agents:write · Returns 201
| Field | Type | Notes |
|---|---|---|
name | string | Required. Unique in the workspace; a duplicate returns 409 conflict |
description | string | Optional, up to 500 characters |
prompt_template | string | Required. Up to 8,000 tokens; longer returns 400 prompt_too_long |
curl -X POST https://api.cleanlist.ai/api/v2/agents/custom \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Series B fit",
"description": "Scores against our Series B ICP",
"prompt_template": "For each lead, judge whether the company matches our Series B ICP and explain why in two sentences."
}'{
"agent_id": "7d2e9b41-...",
"name": "Series B fit",
"description": "Scores against our Series B ICP",
"prompt_template": "For each lead, judge whether the company matches our Series B ICP and explain why in two sentences.",
"prompt_tokens": 21,
"created_at": "2026-11-10T15:00:00",
"modified_at": "2026-11-10T15:00:00",
"timestamp_ms": 1751404800000
}PATCH /agents/custom/{agent_id}
Scope: smart_agents:write
Changes only the fields you send (name, description, prompt_template); an empty body returns 422. Send "description": null to clear the description. An agent_id from another workspace returns 404 not_found. Returns the same object as create.
Agent profile
GET /agents/profile
Scope: smart_agents:read · Cost: free
Returns the profile every Research and Qualification run is measured against. It's read-only here; edit it on the Agents page Profile tab in the app.
{
"organization_id": "org_...",
"website_url": "https://example.com",
"linkedin_url": "https://www.linkedin.com/company/example",
"profile_data": { "icp": "...", "personas": ["..."], "competitors": ["..."] },
"onboarding_completed_at": "2026-10-02T18:20:00",
"last_edited_at": "2026-10-20T09:41:00",
"timestamp_ms": 1751404800000
}profile_data holds your ICP, personas, value props, competitors and exclusions. onboarding_completed_at is null until someone in the workspace approves the profile, and scores are weak until then. A workspace with no profile yet returns 404 not_found: complete agent onboarding in the app first.
Errors
| Code | HTTP | Cause |
|---|---|---|
validation_error | 400 | The selection resolved to no leads of the list, a cohort holds more than 500 of them, or the estimate got a Research scope other than company, contact or both |
approval_required | 400 | scope: "all" on a list of more than 500 leads. Pass lead_ids batches of up to 500 |
quote_expired, quote_mismatch, quote_invalid, spend_cap_exceeded | 400 | Quote again with tool: "run_agent", the list_id, agent_type and (Research) scope |
insufficient_credits | 400 | Not enough credits for the hold; buy more on the Plans page (app.cleanlist.ai/plans) or run a smaller batch |
prompt_too_long | 400 | A prompt over 8,000 tokens |
invalid_cursor | 400 | A results cursor that didn't come from the previous page |
forbidden | 403 | Creating or editing a custom agent without org-admin rights |
feature_gated | 403 | AppSumo-only workspace |
list_not_found | 404 | Unknown list, one you can't access, or one shared with you as view-only |
task_not_found | 404 | An unknown or expired cohort task_id (handles last an hour) |
run_not_found | 404 | Unknown run id, or one from another workspace |
not_found | 404 | Unknown custom agent, or no agent profile yet |
quote_already_redeemed | 409 | Quotes are single use; quote again |
idempotency_conflict | 409 | The idempotency_key was used with different inputs |
conflict | 409 | A custom agent with that name already exists |
rate_limited | 429 | More than 20 runs a minute in the workspace |
See Errors & Rate Limits for every code.