TL;DR: POST /public/enrich/bulk accepts up to 250 contacts per request and returns a workflow_id immediately. Track progress via GET /public/enrich/status or supply a webhook_url to receive the full result set when the workflow finishes.
Enrichment
The Public API exposes two enrichment endpoints:
| Method | Path | Purpose |
|---|---|---|
POST | /api/v1/public/enrich/bulk | Submit a batch of contacts for enrichment |
GET | /api/v1/public/enrich/status | Poll workflow or task status |
Behind the scenes, every enrichment runs through Cleanlist's waterfall provider system, which tries multiple providers in sequence until it finds reliable data.
Bulk enrichment
POST /api/v1/public/enrich/bulk
Submit up to 250 contacts per request. The endpoint validates the input, queues a Temporal workflow, and returns immediately.
curl -X POST https://api.cleanlist.ai/api/v1/public/enrich/bulk \
-H "Authorization: Bearer clapi_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"enrichment_type": "partial",
"webhook_url": "https://your-app.com/webhooks/cleanlist",
"contacts": [
{
"first_name": "John",
"last_name": "Doe",
"company_domain": "acme.com"
},
{
"linkedin_url": "https://www.linkedin.com/in/janedoe"
},
{
"first_name": "Sam",
"last_name": "Lee",
"company_name": "Globex"
}
]
}'Request body
| Field | Type | Required | Description |
|---|---|---|---|
enrichment_type | string | No (defaults to "partial") | One of none, partial, phone_only, full, prospecting_only — see Enrichment Types |
webhook_url | string (URL) | No | If supplied, Cleanlist POSTs the full result set here when the workflow completes — see Webhooks |
contacts | array | Yes | 1–250 contact objects |
Contact object
Each contact must include either:
linkedin_url(preferred — yields the highest match rate), orfirst_name+last_name+ (company_domainorcompany_name)
| Field | Type | Notes |
|---|---|---|
linkedin_url | string | LinkedIn profile URL |
first_name | string | Required if no LinkedIn URL |
last_name | string | Required if no LinkedIn URL |
company_domain | string | e.g., "acme.com" — preferred over name |
company_name | string | Used as a fallback when no domain is available |
A 422 response is returned if any contact fails validation, with the offending row index in the error detail.
Response
{
"workflow_id": "public_bulk_4d6f2c3a-1b2e-4a5b-9c8d-3e2f1a0b9d8e",
"status": "started",
"message": "Bulk enrichment workflow started successfully",
"task_ids": [
"task_uuid_1",
"task_uuid_2",
"task_uuid_3"
],
"total_contacts": 3
}| Field | Description |
|---|---|
workflow_id | Use this to poll status or correlate webhook deliveries |
status | Initial workflow state (started or running) |
task_ids | One per contact — useful for fine-grained polling |
total_contacts | Echo of the input length |
Limits and errors
| Condition | Status | Detail |
|---|---|---|
Empty contacts array | 400 | "At least one contact is required." |
| More than 250 contacts | 400 | "Bulk enrichment supports up to 250 contacts per request." |
| Contact missing required fields | 422 | Pydantic validation error |
| Insufficient credits | 402 | See Credits |
| Invalid API key | 401 | See Authentication |
| Rate limit exceeded | 429 | See Errors |
Polling status
GET /api/v1/public/enrich/status
Poll either by workflow_id (for the entire batch) or by task_id (for a single contact). Provide exactly one — supplying both or neither returns 400.
# Workflow-level
curl "https://api.cleanlist.ai/api/v1/public/enrich/status?workflow_id=public_bulk_4d6f2c3a-..." \
-H "Authorization: Bearer clapi_your_api_key"
# Task-level
curl "https://api.cleanlist.ai/api/v1/public/enrich/status?task_id=task_uuid_1" \
-H "Authorization: Bearer clapi_your_api_key"Query parameters
| Parameter | Type | Description |
|---|---|---|
workflow_id | string | UUID-prefixed workflow id from a POST /enrich/bulk response |
task_id | string | Task UUID returned in the same response |
You must supply exactly one. Both or neither → 400.
Response
When polling by workflow_id:
{
"workflow": {
"workflow_id": "public_bulk_4d6f2c3a-...",
"status": "completed",
"total_prospects": 3,
"completed_prospects": 3,
"failed_prospects": 0,
"emails_found": 2,
"phones_found": 0,
"started_at": "2026-04-06T15:00:00Z",
"completed_at": "2026-04-06T15:00:42Z"
}
}When polling by task_id:
{
"task": {
"task_id": "task_uuid_1",
"workflow_id": "public_bulk_4d6f2c3a-...",
"status": "completed",
"result": {
"primary_email": "john.doe@acme.com",
"primary_email_status": "reliable",
"personal_phone_numbers": []
}
}
}The response uses response_model_exclude_none=True, so optional fields without values are omitted.
Workflow status values
| Status | Meaning |
|---|---|
pending | Workflow accepted, not yet running |
running | Some tasks still in flight |
completed | All tasks finished successfully |
completed_with_errors | Workflow finished, but at least one task failed |
failed | Workflow failed entirely |
Polling vs webhooks
You don't have to choose — both can be used at once.
| Approach | Best for |
|---|---|
Polling GET /enrich/status | Short-lived scripts, dev/test, jobs you control end-to-end |
Webhooks (webhook_url in bulk request) | Production pipelines, serverless functions, anything where you don't want to keep a connection open |
When you supply webhook_url, Cleanlist POSTs the complete result set to your endpoint when the workflow finishes. See Webhooks for the payload schema, retry behavior, and delivery logs.
DEFAULT lead list
Every Public API enrichment is automatically attached to a system-managed lead list named DEFAULT so the contacts remain visible in the Cleanlist portal. You don't need to create this list — it's created on first use and reused thereafter. See Lead Lists for the full explanation.
Related
- Enrichment Types — what each
enrichment_typereturns - Webhooks — outbound payload schema and delivery logs
- Credits — how billing works
- Waterfall enrichment — how Cleanlist combines providers
- API quickstart — end-to-end walkthrough