Skip to Content
New: meet Clu, and send email, LinkedIn and call steps with Sequences. Watch the videos →
GuidesAPI 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 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

  1. Open app.clugtm.com  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 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 -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

  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.