New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
Guides
API Quickstart
🔌

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

  1. Open app.cleanlist.ai (opens in a new tab) and go to Settings → API keys.
  2. Under Create API key, enter a Name such as quickstart-test (optional) and leave Expiry empty or pick a date.
  3. Click Create API key.
  4. 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

  1. A working clapi_ key
  2. A credit check
  3. A bulk enrichment running in the background
  4. A webhook receiver
  5. 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.

Related