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 9. 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 lead_list_id and a webhook_url → handle the enrichment.completed callback → poll as a fallback → audit deliveries. Starting a new integration? Use the v2 quickstart; v2 can also send signed webhook events.
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 CluGTM workspace on a plan with API access (Pro, Enterprise, or AppSumo Tier 5 to 9)
- A few credits
- A lead list you can edit to save the results to (not Extension Leads)
- A public HTTPS endpoint that accepts POST requests (for testing, webhook.site gives you one instantly)
Step 1: Create an API key
- Open app.clugtm.com 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
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
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 lead_list_id, the id of the list the contacts are saved to (required), and webhook_url to get the results pushed to you. The list must be in your API key’s workspace, the key’s user must be able to edit it, and its per-list contact limit applies; Extension Leads can’t be used. Copy a list’s id from its URL in the app (app.cleanlist.ai/lists/<id>), or create a list first with POST /api/v2/lead-lists.
cURL
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",
"lead_list_id": "your_lead_list_id",
"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 the list you passed as lead_list_id.
Running the same example twice? Contacts already in that 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, CluGTM 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. CluGTM 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.