Skip to Content
New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
🔌

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: pip install cleanlist-ai, then Cleanlist(access_token="clapi_…"), then call typed methods like cl.workspace.whoami() and cl.enrichment.enrich_person(...). Sync and async clients included.

SDKs

Official, typed client libraries for API v2, generated from the v2 OpenAPI schema. Available for Python, TypeScript/JavaScript and Java:

LanguagePackageSource
Pythoncleanlist-ai (sync + async)python-client 
TypeScript / JS@cleanlist-ai/sdk (Node + browser)typescript-client 
Javaai.cleanlist:cleanlist-java (OkHttp/Gson)java-client 

This page walks through the Python SDK end to end; TypeScript and Java mirror it with the same Cleanlist client and method names (see each repo’s README).

Prefer raw HTTP? Every page in this section shows cURL. The SDKs are optional sugar over the same https://api.cleanlist.ai/api/v2 surface.

Working from a terminal or a coding agent (Claude Code, Cursor, Codex)? The CluGTM CLI (npx @cleanlist-ai/cli) covers the same API with browser sign-in, CSV in and out, and a quote before paid work.

What the SDKs cover: workspace (identity, credits, estimates, API keys, usage), lead lists, People Finder, enrichment, smart agents and export. Company search, CRM and sequencer sync, webhooks, actions and GTM agents are not in the SDKs yet; call them over HTTP with the same key.

Install

pip install cleanlist-ai

Python 3.8+. Pulls in both the sync (urllib3) and async (aiohttp) runtimes, so both clients work out of the box.

Authenticate

Create a key in the app under Settings → API keys → Create API key (it starts with clapi_ and is shown once), then either pass it explicitly or export it and let the SDK pick it up:

export CLEANLIST_API_KEY="clapi_your_actual_key"
from cleanlist_ai import Cleanlist cl = Cleanlist() # reads CLEANLIST_API_KEY

Your first calls

whoami and credits/balance are free, a good end-to-end sanity check:

from cleanlist_ai import Cleanlist with Cleanlist() as cl: me = cl.workspace.whoami() print(f"Org: {me.organization_name} | tier: {me.tier}") print(f"Credits: {cl.workspace.credits_balance().credits}")

The client exposes six resource groups, mirroring the API sections:

GroupAccessorCovers
Workspacecl.workspaceidentity, credits, estimates, API keys, usage
Lead Listscl.lead_listscreate and manage lists and leads
People Findercl.people_finder (cl.peopleFinder in TypeScript and Java)search people, filter values, compile preview, find people at companies
Enrichmentcl.enrichmentperson, company, by-task and bulk enrichment + status
Smart Agentscl.smart_agentsrun AI columns and read results
Exportcl.exportCSV (signed URL) and JSON export

Enrich a person and poll

Enrichment is asynchronous: the call starts a workflow and returns a workflow_id; poll enrichment_status until it settles.

import time from cleanlist_ai import Cleanlist from cleanlist_ai.models import CreateListRequest, EnrichPersonRequest TERMINAL = {"completed", "completed_with_errors", "failed", "canceled", "terminated"} with Cleanlist() as cl: lst = cl.lead_lists.create_list(CreateListRequest(name="SDK demo")) job = cl.enrichment.enrich_person( EnrichPersonRequest( lead_list_id=lst.list_id, first_name="Ada", last_name="Lovelace", company_name="Analytical Engines", ) ) print("workflow:", job.workflow_id, "| reserved:", job.credits_reserved) while True: status = cl.enrichment.enrichment_status(job.workflow_id) if status.status in TERMINAL: break time.sleep(3) print("charged:", status.credits_charged, "refunded:", status.credits_refunded)

Bulk enrichment (estimate, quote, run)

Bulk operations must be priced with a quote first. Call credits_estimate to get a signed, single-use quote_id, then pass it to enrich_list:

from cleanlist_ai.models import EstimateCostRequest, EnrichListRequest quote = cl.workspace.credits_estimate( EstimateCostRequest(tool="enrich_list", list_id=lst.list_id, scope="full") ) print(f"cost={quote.estimated_cost} sufficient={quote.sufficient}") if quote.sufficient: run = cl.enrichment.enrich_list( EnrichListRequest(list_id=lst.list_id, scope="full", quote_id=quote.quote_id) ) print("bulk workflow:", run.workflow_id)

Scopes: partial (email + LinkedIn + title + company, 1 credit) · phone-only (10 credits) · full (email and phone, 11 credits). Pricing is pay-for-results: the reservation is a cap and the unused part is refunded.

Async

The async client lives under cleanlist_ai.aio with identical method names. Use it as an async context manager and await each call; asyncio.gather fires independent requests concurrently:

import asyncio from cleanlist_ai.aio import Cleanlist async def main(): async with Cleanlist() as cl: me, balance = await asyncio.gather( cl.workspace.whoami(), cl.workspace.credits_balance(), ) print(me.organization_name, balance.credits) asyncio.run(main())

Error handling

Non-2xx responses raise a typed ApiException subclass carrying status, reason and the parsed body:

from cleanlist_ai import ApiException from cleanlist_ai.exceptions import NotFoundException, UnauthorizedException try: cl.lead_lists.get_list("does-not-exist") except NotFoundException: print("no such list") except UnauthorizedException: print("bad or missing API key") except ApiException as e: print(f"API error {e.status}: {e.body}")

The body holds the error envelope; branch on its code. See Errors & Rate Limits for every code.

Other languages

Official SDKs with the same Cleanlist facade and method names:

Need another language? Generate a client from the OpenAPI spec with openapi-generator . The served spec includes POST /credits/estimate, so generated clients can get quotes, but it leaves out company search and sync, so add those calls by hand.

Next steps