Audience: developers. API keys and the REST API are included on Pro and Enterprise (and legacy Pro and Scale plans) and on AppSumo Tiers 5 to 7. They are not available on Free, Starter, AppSumo Tiers 1 to 4 or during the Pro trial. Support agent: confirm the user's plan includes API access before giving API advice.
TL;DR: Create a key in Settings → API keys → check it → check your credits → POST /enrich/bulk with a webhook_url → handle the enrichment.completed callback → poll as a fallback → audit deliveries. Starting a new integration that doesn't need webhooks? Use the v2 quickstart.
API quickstart (v1 with webhooks)
Goal: go from no API key to enrichment results arriving at your server by webhook in about 10 minutes.
Prerequisites
- A Cleanlist workspace on a plan with API access (Pro, Enterprise, or AppSumo Tier 5 to 7)
- A few credits
- A public HTTPS endpoint that accepts POST requests (for testing, webhook.site (opens in a new tab) gives you one instantly)
Step 1: Create an API key
- Open app.cleanlist.ai (opens in a new tab) and go to Settings → API keys.
- Under Create API key, enter a Name such as
quickstart-test(optional) and leave Expiry empty or pick a date. - Click Create API key.
- Copy the key under New API key. It starts with
clapi_and is shown only once.
Store it in an environment variable:
export CLEANLIST_API_KEY="clapi_your_actual_key"Step 2: Check the key
A free sanity check:
curl https://api.cleanlist.ai/api/v1/public/auth/validate-key \
-H "Authorization: Bearer $CLEANLIST_API_KEY"You should see {"valid": true, "user_id": "...", "organization_id": "..."}. A 401 means the key was copied wrong or revoked; a 403 means the workspace's plan doesn't include API access.
Step 3: Check your credits
The v1 API has no balance endpoint, so use the v2 one with the same key (scope credits:read, free). You can also see your balance in the credits row at the bottom of the app sidebar.
curl https://api.cleanlist.ai/api/v2/credits/balance \
-H "Authorization: Bearer $CLEANLIST_API_KEY"The example below uses partial on 2 contacts, so the balance must be at least 2 credits before the run starts. You pay 1 credit per email found.
Step 4: Start a bulk enrichment
Pass webhook_url to get the results pushed to you:
curl -X POST https://api.cleanlist.ai/api/v1/public/enrich/bulk \
-H "Authorization: Bearer $CLEANLIST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enrichment_type": "partial",
"webhook_url": "https://webhook.site/your-unique-url",
"contacts": [
{
"first_name": "John",
"last_name": "Doe",
"company_domain": "acme.com"
},
{
"linkedin_url": "https://www.linkedin.com/in/janedoe"
}
]
}'The response comes back right away; the enrichment runs in the background. The contacts are also saved to your Extension Leads list in the app.
Running the same example twice? Contacts already in your Extension Leads list are skipped, and if every contact is a duplicate you get 400 "All 2 prospects are duplicates already in this list." Use different contacts for the second run.
Step 5a: Receive the webhook
When the run finishes, Cleanlist sends one POST to your webhook_url:
{
"event": "enrichment.completed",
"workflow_id": "public_bulk_4d6f2c3a-...",
"lead_list_id": "550e8400-...",
"enrichment_type": "partial",
"status": "completed",
"summary": {
"total": 2,
"successful": 2,
"failed": 0,
"missed": 0,
"errors": 0,
"throttled": 0,
"emails_found": 2,
"phones_found": 0
},
"results": [
{
"task_id": "task_id_1",
"status": "completed",
"prospect_id": "prospect_...",
"lead_id": "lead_...",
"result": {
"email": "john.doe@acme.com",
"email_status": "verified",
"other_emails": [],
"phones": [],
"provider": "wiza",
"linkedin_url": "https://www.linkedin.com/in/johndoe"
},
"error": null,
"input": {
"linkedin_url": null,
"first_name": "John",
"last_name": "Doe",
"full_name": "John Doe",
"company_domain": "acme.com",
"company_name": null
}
}
],
"results_limit": 500,
"results_truncated": 0,
"results_endpoint": "/api/v1/public/enrich/status?workflow_id=public_bulk_4d6f2c3a-...",
"completed_at": "2026-10-07T15:00:42Z"
}status can be completed, completed_with_errors or failed, and summary.failed splits into missed (nothing found), errors and throttled (both worth retrying). See Webhooks.
A minimal Express receiver:
import express from "express";
const app = express();
app.use(express.json({ limit: "10mb" }));
app.post("/webhooks/cleanlist", (req, res) => {
const { workflow_id, status, summary } = req.body;
console.log("Received:", workflow_id, status);
console.log(`${summary.successful} of ${summary.total} enriched`);
// Save the payload, then answer 200 quickly.
res.status(200).end();
});
app.listen(3000);See Receiving webhooks for production patterns.
Step 5b: Or poll for status
If you'd rather poll (or want a fallback):
import time
TERMINAL = {"completed", "completed_with_errors", "failed"}
def wait_for_workflow(workflow_id, poll_seconds=5):
while True:
r = requests.get(
"https://api.cleanlist.ai/api/v1/public/enrich/status",
headers=HEADERS,
params={"workflow_id": workflow_id},
)
r.raise_for_status()
wf = r.json().get("workflow", {})
print(f" status={wf.get('status')}")
if wf.get("status") in TERMINAL:
return wf
time.sleep(poll_seconds)
final = wait_for_workflow(workflow_id)
print(final["emails_found"], "emails found")The run-level poll returns counts. For each contact's data, poll ?task_id=... for each entry in task_ids (kept for 24 hours). See Enrichment.
Step 6: Audit webhook deliveries
To confirm the callback reached you:
deliveries = requests.get(
"https://api.cleanlist.ai/api/v1/public/webhooks/deliveries",
headers=HEADERS,
params={"workflow_id": workflow_id},
).json()
for attempt in deliveries:
print(
f"Attempt #{attempt['attempt_number']}: "
f"{attempt['status']} ({attempt['response_status_code']}) "
f"in {attempt['duration_ms']}ms"
)You get one row per attempt, newest first. Cleanlist makes up to 5 attempts.
What you've built
- A working
clapi_key - A credit check
- A bulk enrichment running in the background
- A webhook receiver
- Visibility into delivery attempts
Next steps:
- Wire it into your ETL job or CRM pipeline.
- Create a separate key per environment (see Authentication).
- Read Errors and rate limits for production error handling.
- Research or score the enriched contacts with a skill in the app.