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: GET /credits/balance returns an integer balance. Paid bulk operations need a signed quote_id from POST /credits/estimate (5-minute TTL, single use). Running short returns 400 insufficient_credits with the exact shortfall. GET /usage reports spend over time. Reads, saves and status polls are free.
Credits
Cleanlist API calls that search, read, save, estimate or poll are free. A work email costs 1 credit, a direct dial 10 and both on one row 11, and a lookup that finds nothing costs 0.
Cleanlist bills enrichment, smart agents and sync in credits shared by the whole organization. This page covers the three credit endpoints and the full cost table.
Balance
GET /credits/balance
Scope: credits:read · Cost: free
curl https://api.cleanlist.ai/api/v2/credits/balance \
-H "Authorization: Bearer clapi_your_api_key"{
"organization_id": "org_2xyz...",
"credits": 4820,
"timestamp_ms": 1717430400000,
"agent_instructions": "You have 4820 credits remaining."
}credits is a plain integer: the credits available to your whole organization.
Estimates and the quote model
POST /credits/estimate
Scope: credits:read · Cost: free
Prices a paid operation. Returns a quote_id bound (with HMAC-SHA256) to the exact request shape: tool, list or cohort, scope, row count, destination and your organization. The quote lasts 5 minutes and can be used once. At execution the server recomputes the real cost and rejects the call with spend_cap_exceeded if it would exceed the quote.
When you pass a list_id (or a cohort task_id), the row count is always derived on the server; any row_count you send is ignored, so you can't under-quote a large list.
POST /credits/estimate is live but not in the served OpenAPI spec. Call it over HTTP, or use workspace.credits_estimate(...) in the SDKs.
Request body
| Field | Type | Used for |
|---|---|---|
tool | string | Required. The operation to price (see the cost table). Unknown tools return 400 |
list_id | string | List-backed tools (enrich_list, run_smart_agent, sync_to_crm, sync_to_sequencer); row count is read from the list |
task_id | string | Cohort tools (add_person_enrichments, add_company_enrichments); row count is read from the cohort |
entity_ids | string[] | Optional subset of a cohort to price |
data_points | string[] | Cohort person enrichment: Email, Phone, LinkedIn |
row_count | integer | Tools that aren't list- or cohort-backed (for example bulk_import_csv) |
scope | string | enrich_list: partial, full or phone-only |
enrichment_type | string | enrich_person and bulk_import_csv: partial, phone_only or full |
agent_type | string | run_smart_agent: custom_ai, cold_intro_email, title_normalizer, company_intel or pain_point_hypothesis |
column_name, prompt, template_id, lead_scope, max_rows, overwrite | various | run_smart_agent: must match the run request exactly |
provider | string | Sync: hubspot or salesforce (CRM), lemlist (sequencer) |
sub_action | string | sync_to_crm: contacts for HubSpot; leads (default) or contacts for Salesforce |
campaign_id | string | sync_to_sequencer: the Lemlist campaign |
field_mapping | object | Sync: must match the sync request exactly |
include_phone | boolean | Legacy person pricing flag; prefer enrichment_type |
Response
{
"quote_id": "qte_v1_1717430400_3f2c...e9",
"expires_at": 1717430700,
"estimated_cost": 110,
"tool": "enrich_list",
"row_count": 10,
"filter_hash": "a1b2c3d4e5f6a7b8",
"available_credits": 4820,
"sufficient": true
}| Field | Meaning |
|---|---|
quote_id | Pass this to the paid endpoint. Single use within its TTL |
expires_at | Unix seconds; the quote is invalid after this |
estimated_cost | Credits the operation will cost at most (the spend cap) |
row_count | Rows the quote was priced for |
available_credits | Your current balance |
sufficient | true if available_credits >= estimated_cost |
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": "full"}'With an SDK: workspace.credits_estimate(...) (creditsEstimate in TS and Java) returns the signed quote_id. See the SDKs.
Quote lifecycle
- Estimate: mint a
quote_idfor the request shape. - Execute: pass the
quote_idto the paid endpoint. The server checks the signature and recomputes the cost. - Single use: a quote can't be redeemed twice (
quote_already_redeemed: 409 on smart agents and sync, 400 on enrichment). - Expiry: after 5 minutes the quote is rejected (
quote_expired); estimate again. - Shape change: a quote for a different list, scope, destination or mapping is rejected (
quote_mismatch). If the cost grew past the quote, you getspend_cap_exceeded. A row count may drift by up to 10% before the quote is rejected. - Can't confirm: if the server can't confirm the quote is unused, it returns
503 quote_verification_unavailable(retryable).
Insufficient credits
Paid operations check your balance before they start. If you can't cover the cost, you get HTTP 400 with code insufficient_credits:
{
"error": {
"code": "insufficient_credits",
"problem": "This enrich_person call requires 11 credits; organization has 4.",
"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_...",
"estimated_cost_credits": 11,
"available_credits": 4,
"shortfall_credits": 7
}
}The estimated_cost_credits, available_credits and shortfall_credits fields tell you exactly how much to add.
The fix text is returned verbatim by the API and mentions app.cleanlist.ai/billing. That page does not exist: buy credits or change plan on the Plans page at app.cleanlist.ai/plans (opens in a new tab) (Settings → Plans & billing). Credit top-ups aren't available during a trial.
Usage reports
GET /usage
Scope: admin:api_keys · Cost: free
Aggregates your organization's API request log over a window.
| Query param | Default | Range or values |
|---|---|---|
days | 7 | 1 to 365 |
group_by | tool | tool, key, day or error |
curl "https://api.cleanlist.ai/api/v2/usage?days=30&group_by=tool" \
-H "Authorization: Bearer clapi_your_api_key"{
"days": 30,
"group_by": "tool",
"total_calls": 1820,
"total_credits_spent": 312,
"total_errors": 12,
"rows": [
{ "group": "/enrichment/bulk", "calls": 240, "credits_spent": 264, "errors": 3 },
{ "group": "/people/find", "calls": 980, "credits_spent": 0, "errors": 1 }
]
}With group_by=tool, rows are bucketed by the endpoint template (the path without the /api/v2 prefix, for example /enrichment/person or /lead-lists/{list_id}/leads). credits_spent and total_credits_spent are whole numbers: each request's cost is rounded up before it's logged. The figure is the cost reserved at dispatch, not the settled charge after pay-for-results refunds. Free endpoints (search, saving leads) report 0.
Credit cost table
Costs are computed per request and the total is rounded up to a whole credit. Reads are always free. Use the tool name with POST /credits/estimate.
| Operation | Tool name (for estimate) | Cost |
|---|---|---|
| People search | search_people | Free |
| Company search | search_companies | Free |
| Similar companies | find_similar_companies | No credits deducted (the estimate returns 1; no quote needed) |
| Save leads to a list | add_leads_to_list | Free |
| Person enrichment, email + profile | enrich_person (partial) | 1 |
| Person enrichment, phone only | enrich_person (phone_only) | 10 |
| Person enrichment, full | enrich_person (full) | 11 |
| Company enrichment (single) | enrich_company | 1 |
| Cohort person enrichment | add_person_enrichments | Per person: Email 1 + Phone 10 (LinkedIn is free; a LinkedIn-only cohort is rejected) |
| Cohort company enrichment | add_company_enrichments | ceil(n / 10), about 1 credit per 10 companies |
| Bulk list enrichment, partial | enrich_list (partial) | 1 per lead |
| Bulk list enrichment, full | enrich_list (full) | 11 per lead |
| Bulk list enrichment, phone only | enrich_list (phone-only) | 10 per lead |
| CSV import | bulk_import_csv | Import free; enrichment per lead (partial 1, phone_only 10, full 11) |
| Smart agent, cold intro email | run_smart_agent (cold_intro_email) | 3 per lead |
| Smart agent, presets | run_smart_agent (title_normalizer, company_intel, pain_point_hypothesis) | 1 per lead |
| Smart agent, custom AI | run_smart_agent (custom_ai) | Priced from its prompt. Read estimated_cost from the quote |
| Sync to CRM | sync_to_crm | 0.2 per lead |
| Sync to sequencer | sync_to_sequencer | 0.2 per lead |
| Find people at list companies | (no quote) | Free; the job needs a balance of at least companies × limit_per_company to start |
whoami, balance, estimate, list reads, status polls, exports | read endpoints | Free |
Enrichment is pay-for-results: data that isn't found isn't charged. Async enrichments reserve the estimated credits up front and refund the unused part when the workflow settles. Sync charges every lead in the list once the push completes, including leads the destination skipped or rejected. See Enrichment and CRM & Sequencer Sync.