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

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.

EndpointMethodScopeCost
Run QualificationPOST /agents/qualification/runsmart_agents:writePer lead, from the quote
Run ResearchPOST /agents/research/runsmart_agents:writePer lead and research_scope, from the quote
Run statusGET /agents/runs/{run_id}smart_agents:readFree
Run resultsGET /agents/runs/{run_id}/resultssmart_agents:readFree
List agentsGET /agentssmart_agents:readFree
Create custom agentPOST /agents/customsmart_agents:writeFree
Update custom agentPATCH /agents/custom/{agent_id}smart_agents:writeFree
Agent profileGET /agents/profilesmart_agents:readFree

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

AgentWhat each lead getsPrice
qualificationA 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 flagscredits_per_lead in GET /agents
researchLive web research on the company, the contact or both, set by research_scopecredits_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

FieldTypeNotes
list_idstringRequired. A list you can edit. A list you can't access, or can only view, returns 404 list_not_found
lead_idsstring[]1 to 500 lead ids. Ids that aren't in the list are dropped
task_idstringA 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_scopeenumResearch only: company, contact or both (default both). Sets depth and price
quote_idstringRequired. From POST /credits/estimate; see below
idempotency_keystringOptional, 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
}
FieldMeaning
run_idThe run handle. Poll it at status_url
lead_countLeads actually scheduled, after dropping ids outside the list
credits_reservedCredits 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_leadThe 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 paramDefaultNotes
min_fit_scorenone0 to 100. Keeps rows scoring at least this; rows with no score are dropped
verdictnonequalified, disqualified or needs_review
sortfit_score_descOr fit_score_asc. Unscored rows always sort last
limit251 to 100
cursornoneFrom 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
}
FieldMeaning
total_rowsRows from this run, before min_fit_score and verdict filter them
counts_by_verdictVerdict counts over the whole run, never narrowed by the filters. unscored counts rows with no verdict yet
fit_score0 to 100, or null when the row isn't scored yet or the skill could only return prose (structured_failed: true)
crm_statusThe CRM check: is_existing and matched_record, or an error of no_crm_connected or lookup_timeout
analysisThe 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
errorWhy 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

FieldTypeNotes
namestringRequired. Unique in the workspace; a duplicate returns 409 conflict
descriptionstringOptional, up to 500 characters
prompt_templatestringRequired. 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

CodeHTTPCause
validation_error400The 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_required400scope: "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_exceeded400Quote again with tool: "run_agent", the list_id, agent_type and (Research) scope
insufficient_credits400Not enough credits for the hold; buy more on the Plans page (app.cleanlist.ai/plans) or run a smaller batch
prompt_too_long400A prompt over 8,000 tokens
invalid_cursor400A results cursor that didn't come from the previous page
forbidden403Creating or editing a custom agent without org-admin rights
feature_gated403AppSumo-only workspace
list_not_found404Unknown list, one you can't access, or one shared with you as view-only
task_not_found404An unknown or expired cohort task_id (handles last an hour)
run_not_found404Unknown run id, or one from another workspace
not_found404Unknown custom agent, or no agent profile yet
quote_already_redeemed409Quotes are single use; quote again
idempotency_conflict409The idempotency_key was used with different inputs
conflict409A custom agent with that name already exists
rate_limited429More than 20 runs a minute in the workspace

See Errors & Rate Limits for every code.

Related