New: API Reference docs are live — integrate Cleanlist enrichment into your apps. View API docs →
Guides
API Quickstart

TL;DR: Generate a key → check credits → submit a bulk enrichment with webhook_url → handle the webhook → audit deliveries. This guide walks you through it end-to-end with copy-paste examples.

API Quickstart

Goal: get from "no API key" to "enrichment results landing in your app via webhook" in under 10 minutes.

Prerequisites

Step 1 — Generate an API key

  1. Log in to portal.cleanlist.ai (opens in a new tab)
  2. Navigate to Settings → API Keys
  3. Click Generate Key
  4. Give it a name (e.g., quickstart-test)
  5. Copy the full key — it starts with clapi_ and is shown once

Store it in an environment variable:

export CLEANLIST_API_KEY="clapi_your_actual_key"

Step 2 — Confirm the key works

A fast, zero-credit 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": "..."}.

Step 3 — Check your credit balance

curl https://api.cleanlist.ai/api/v1/public/credits/balance \
  -H "Authorization: Bearer $CLEANLIST_API_KEY"

You'll need at least a few credits to run the example below.

Step 4 — Submit a bulk enrichment

Pass webhook_url to receive results when the workflow finishes:

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 is immediate. The workflow runs in the background.

Step 5a — Receive the result via webhook

When the workflow finishes, Cleanlist POSTs to your webhook URL with a payload like:

{
  "event": "enrichment.completed",
  "workflow_id": "public_bulk_4d6f2c3a-...",
  "lead_list_id": "550e8400-...",
  "enrichment_type": "partial",
  "status": "completed",
  "summary": {
    "total": 2,
    "successful": 2,
    "failed": 0,
    "emails_found": 2,
    "phones_found": 0
  },
  "results": [
    {
      "task_id": "task_1",
      "primary_email": "john.doe@acme.com",
      "primary_email_status": "reliable",
      "prospect": { "first_name": "John", "last_name": "Doe" },
      "company": { "name": "Acme Corp", "domain": "acme.com" }
    }
  ],
  "completed_at": "2026-04-06T15:00:42Z"
}

A minimal Express receiver:

import express from "express";
const app = express();
app.use(express.json({ limit: "10mb" }));
 
app.post("/webhooks/cleanlist", (req, res) => {
  console.log("Received:", req.body.workflow_id, req.body.status);
  console.log(`${req.body.summary.successful} of ${req.body.summary.total} enriched`);
  // ... persist results, then 200 to ack
  res.status(200).end();
});
 
app.listen(3000);

See the Receiving Webhooks guide for production patterns.

Step 5b — Or poll for status

If you'd rather poll than use webhooks (or both):

import time
 
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", {})
        status = wf.get("status")
        print(f"  status={status}")
        if status in {"completed", "completed_with_errors", "failed"}:
            return wf
        time.sleep(poll_seconds)
 
final = wait_for_workflow(workflow_id)
print(f"Done: {final}")

Step 6 — Audit webhook deliveries

If your webhook endpoint is flaky and you want to confirm delivery actually happened, query the delivery log:

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'll see one row per attempt, including any retries.

What you've built

You now have:

  1. ✓ A working clapi_ API key
  2. ✓ A live credit-balance check
  3. ✓ A bulk enrichment workflow running asynchronously
  4. ✓ A webhook-based result handler
  5. ✓ Visibility into delivery attempts

From here you can:

  • Wire it into your real ETL or CRM pipeline
  • Layer on Smart Columns in the portal for richer enrichment (see Smart Columns)
  • Set up multiple keys for dev / staging / production (see Authentication)
  • Read the Errors reference for production-ready error handling

Related