TL;DR: GET /credits/balance returns an integer balance. Paid bulk operations need a signed quote_id from POST /credits/estimate (5-minute TTL). Running short returns 400 insufficient_credits with exact shortfall. GET /usage reports spend over time. Reads and status polls are free.
Credits
Cleanlist bills enrichment and sync work in credits. 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/v1/public/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 & the quote model
POST /credits/estimate
Scope: credits:read · Cost: free
Pre-flight a paid operation. Returns a quote_id bound (via HMAC-SHA256) to the exact request shape — tool, list/filters, scope, row count, and your org. The quote has a 5-minute TTL. Paid bulk endpoints accept (and in some cases require) this quote_id; at execution time the server recomputes the real cost and rejects with spend_cap_exceeded if it would exceed the quote.
When you pass a list_id, the row count is always derived from the list — your row_count is ignored, so you can't under-quote a large list.
Request body
| Field | Type | Notes |
|---|---|---|
tool | string | Required. The operation to price (see the cost table below) |
list_id | string | For list-scoped tools; row count is read from the list |
row_count | integer | For non-list tools (e.g. by-task / CSV) — ignored when list_id is set |
scope | string | For enrich_list: partial | full | phone-only |
enrichment_type | string | For bulk_import_csv: partial | phone_only | full |
agent_type | string | For run_smart_agent: e.g. custom_ai, cold_intro_email |
include_phone | boolean | Legacy flag for person pricing |
provider | string | For sync tools |
sub_action | string | For sync_to_crm composite syncs (e.g. contacts+companies) |
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 (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/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": "full"}'Quote lifecycle
- Estimate → mint a
quote_idfor the request shape. - Execute → pass
quote_idto the paid endpoint. The server verifies the signature and recomputes cost. - Single use → a quote can't be redeemed twice (
quote_already_redeemed). - Expiry → after ~5 minutes the quote is rejected (
quote_expired); just re-estimate. - Drift → if the actual cost exceeds the quote, you get
spend_cap_exceeded— re-estimate against the current shape.
Insufficient credits
Paid operations check your balance up front. If you can't cover the cost, you get HTTP 400 with code insufficient_credits (not 402):
{
"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.
Usage reports
GET /usage
Scope: admin:api_keys · Cost: free
Aggregate your organization's API request log over a window.
| Query param | Default | Range / values |
|---|---|---|
days | 7 | 1–365 |
group_by | tool | tool | key | day | error |
curl "https://api.cleanlist.ai/api/v1/public/usage?days=30&group_by=tool" \
-H "Authorization: Bearer clapi_your_api_key"{
"days": 30,
"group_by": "tool",
"total_calls": 1820,
"total_credits_spent": 0,
"total_errors": 12,
"rows": [
{ "group": "/api/v1/public/enrichment/bulk", "calls": 240, "credits_spent": 0, "errors": 3 },
{ "group": "/api/v1/public/search/people", "calls": 980, "credits_spent": 0, "errors": 1 }
]
}The usage log currently buckets group_by="tool" by request path, and credits_spent is reported as 0 (the request log does not yet carry a per-row credit cost). Track real spend by reading GET /credits/balance over time.
Credit cost table
Costs are computed per request and rounded up (ceiling). Reads are always free.
| Operation | Tool name (for estimate) | Cost |
|---|---|---|
| People search | search_people | 0.5 / lead (deferred to enrich/save) |
| Company search | search_companies | 0.5 / lead (deferred to enrich/save) |
| Similar companies | find_similar_companies | 1 / call |
| Person enrichment — email only | enrich_person (partial) | 1 |
| Person enrichment — phone only | enrich_person (phone_only) | 10 |
| Person enrichment — full | enrich_person (full) | 11 |
| Company enrichment | enrich_company | 1 |
| Bulk list enrichment — partial | enrich_list (partial) | 1 / lead |
| Bulk list enrichment — full | enrich_list (full) | 11 / lead |
| Bulk list enrichment — phone only | enrich_list (phone-only) | 10 / lead |
| CSV import | bulk_import_csv | 0 import + per-lead enrich (partial 1 / phone_only 10 / full 11) |
| Smart agent — custom AI | run_smart_agent (custom_ai) | 1 / lead |
| Smart agent — cold intro email | run_smart_agent (cold_intro_email) | 3 / lead |
| Sync to CRM | sync_to_crm | 0.2 / lead |
| Sync to sequencer | sync_to_sequencer | 0.2 / lead |
whoami, balance, estimate, list reads, status polls, exports | — | free |
Failed enrichments are not charged for data that wasn't found. Async enrichments reserve the estimated credits up front and refund the unused portion when the workflow settles — see Enrichment for the reservation model.