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

TL;DR: Pass webhook_url to /public/enrich/bulk to receive a single POST when the workflow finishes. Cleanlist retries up to 5 times with exponential backoff. Inspect delivery history via GET /public/webhooks/deliveries.

Webhooks

Webhooks are the recommended way to receive enrichment results in production. Instead of polling, you supply a URL and Cleanlist POSTs the complete result set to it once the workflow finishes.

Opting in

Add webhook_url to your bulk enrichment request:

{
  "enrichment_type": "partial",
  "webhook_url": "https://your-app.com/webhooks/cleanlist",
  "contacts": [...]
}

That's the entire setup. There is no separate webhook registration step — webhooks are scoped to a single workflow.

Outbound payload

When the workflow completes, Cleanlist sends a single POST request to your webhook_url:

POST /webhooks/cleanlist HTTP/1.1
Host: your-app.com
Content-Type: application/json
{
  "event": "enrichment.completed",
  "workflow_id": "public_bulk_4d6f2c3a-1b2e-4a5b-9c8d-3e2f1a0b9d8e",
  "lead_list_id": "550e8400-e29b-41d4-a716-446655440000",
  "enrichment_type": "partial",
  "status": "completed",
  "summary": {
    "total": 3,
    "successful": 2,
    "failed": 1,
    "emails_found": 2,
    "phones_found": 0
  },
  "results": [
    {
      "task_id": "task_uuid_1",
      "status": "completed",
      "primary_email": "john.doe@acme.com",
      "primary_email_status": "reliable",
      "personal_phone_numbers": [],
      "prospect": {
        "first_name": "John",
        "last_name": "Doe",
        "full_name": "John Doe"
      },
      "company": {
        "name": "Acme Corp",
        "domain": "acme.com"
      }
    }
  ],
  "results_limit": 250,
  "results_truncated": 0,
  "results_endpoint": "/api/v1/public/enrich/status?workflow_id=public_bulk_4d6f2c3a-...",
  "completed_at": "2026-04-06T15:00:42Z"
}

Field reference

FieldTypeDescription
eventstringAlways "enrichment.completed"
workflow_idstringWorkflow id from the original bulk response
lead_list_idstringUUID of the DEFAULT lead list the contacts were attached to
enrichment_typestringThe type you submitted (none, partial, phone_only, full, prospecting_only)
statusstring"completed" or "completed_with_errors"
summary.totalintTotal contacts in the batch
summary.successfulintNumber of contacts that returned a result
summary.failedintNumber of contacts that did not
summary.emails_foundintTotal emails discovered across the batch
summary.phones_foundintTotal phones discovered across the batch
resultsarrayPer-contact result objects
results_limitintCap on the number of result objects included in this payload
results_truncatedintNumber of results omitted because the batch exceeded results_limit
results_endpointstringWhere to fetch the full result set if results_truncated > 0
completed_atISO-8601 datetimeWhen the workflow finished

If results_truncated > 0, fetch the full result set by calling the endpoint in results_endpoint.

Delivery semantics

PropertyValue
HTTP methodPOST
Content typeapplication/json
Timeout10 seconds per attempt
SuccessAny HTTP 2xx response
RetriesUp to 5 attempts total
Backoff1s, 2s, 4s, 8s, 16s
IdempotencyUse workflow_id as your idempotency key

If your endpoint returns a non-2xx status (or times out), Cleanlist will retry on the next backoff interval. After 5 unsuccessful attempts the delivery is marked failed — you can still retrieve the result set by polling GET /public/enrich/status?workflow_id=....

Cleanlist does not currently sign webhook payloads. Treat the workflow_id as opaque and only trust webhooks for workflows you submitted yourself. Verify the workflow id against your own records before processing the payload.

Inspecting delivery history

GET /api/v1/public/webhooks/deliveries

Pull the delivery log for a specific workflow:

curl "https://api.cleanlist.ai/api/v1/public/webhooks/deliveries?workflow_id=public_bulk_4d6f2c3a-...&limit=50" \
  -H "Authorization: Bearer clapi_your_api_key"

Query parameters

ParameterTypeDefaultRange
workflow_idstringrequired
limitint501–200

Response

[
  {
    "id": "delivery-uuid",
    "webhook_id": "https://your-app.com/webhooks/cleanlist",
    "workflow_id": "public_bulk_4d6f2c3a-...",
    "event_type": "enrichment.completed",
    "attempt_number": 1,
    "status": "delivered",
    "response_status_code": 200,
    "error_message": null,
    "duration_ms": 184,
    "created_at": "2026-04-06T15:00:42Z"
  }
]
FieldDescription
webhook_idThe destination URL Cleanlist POSTed to
attempt_number1-indexed attempt counter (1–5)
status"delivered" or "failed"
response_status_codeHTTP status returned by your endpoint (null on transport errors)
error_messageShort description if the attempt failed
duration_msWall-clock time the attempt took

Each retry creates a new row, so a workflow with 3 failed attempts followed by 1 success will have 4 rows.

Implementing a receiver

A minimal Express handler:

import express from "express";
const app = express();
app.use(express.json({ limit: "10mb" }));
 
const KNOWN_WORKFLOWS = new Set(); // populate from your DB
 
app.post("/webhooks/cleanlist", (req, res) => {
  const { workflow_id, status, results } = req.body;
 
  if (!KNOWN_WORKFLOWS.has(workflow_id)) {
    // Reject unknown workflow ids — defense in depth
    return res.status(404).end();
  }
 
  // Persist results, then return 200 ASAP so Cleanlist marks delivered
  saveResults(workflow_id, results).catch(console.error);
  res.status(200).end();
});
 
app.listen(3000);

A minimal Flask handler:

from flask import Flask, request
 
app = Flask(__name__)
KNOWN_WORKFLOWS = set()  # populate from your DB
 
@app.post("/webhooks/cleanlist")
def cleanlist_webhook():
    body = request.get_json()
    workflow_id = body.get("workflow_id")
 
    if workflow_id not in KNOWN_WORKFLOWS:
        return "", 404
 
    # Process asynchronously and ack quickly
    enqueue_processing(body)
    return "", 200

See the Receiving Webhooks guide for production-ready patterns (background jobs, retries, dead letter queues).

Related