TL;DR: POST to /enrich for single contacts or /enrich/bulk for CSV uploads. Returns verified email, phone, LinkedIn data, and company info via the same waterfall enrichment system as the portal.
Enrichment API
Public enrichment is asynchronous. You submit contacts in bulk, receive identifiers, then poll or use webhooks for completion.
Start Bulk Enrichment
POST /api/v1/public/enrich/bulk
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",
"contacts": [
{
"linkedin_url": "https://www.linkedin.com/in/levon-adamyan/",
"first_name": "Levon",
"last_name": "Adamyan",
"company_domain": "cleanlist.ai"
},
{
"first_name": "Levon",
"last_name": "Adamyan",
"company_name": "Cleanlist"
}
]
}'Request Body
| Field | Type | Required | Description |
|---|---|---|---|
enrichment_type | string | Yes | none, partial, phone_only, or full |
contacts | array | Yes | Up to 250 contacts per request |
webhook_url | string | No | URL to receive a callback when enrichment completes |
Contact Input Requirements
Each contact must include one of these two input patterns:
linkedin_urlfirst_name+last_name+ (company_nameorcompany_domain)
If contacts exceeds 250, the API returns 400 with a validation error.
LinkedIn-Only Payload Example
{
"enrichment_type": "full",
"contacts": [
{
"linkedin_url": "https://www.linkedin.com/in/levon-adamyan/"
}
]
}Enrichment Types
| Type | Behavior | Typical use |
|---|---|---|
none | Normalize and prepare records only | Stage/import contacts without appending new contact data |
partial | Email enrichment | Find and verify business emails |
phone_only | Phone enrichment | Find direct phone numbers |
full | Email + phone enrichment | Max contact coverage |
Response
{
"workflow_id": "public_bulk_3d1f9a67-cc44-4a06-b8d4-7fe7a8f31a01",
"status": "processing",
"message": "Bulk enrichment started",
"task_ids": [
"6aab5e77-f5c0-4233-8f8e-a0e1a9baf18c",
"d2c1e5f2-2d83-47f2-a620-4d27906b9f65"
],
"total_contacts": 2
}The API validates credit availability before processing. If insufficient, the request can return 402.
Poll Enrichment Status
GET /api/v1/public/enrich/status
Provide exactly one query parameter:
workflow_idfor aggregate workflow progresstask_idfor single task status
Workflow Status Example
curl "https://api.cleanlist.ai/api/v1/public/enrich/status?workflow_id=public_bulk_3d1f9a67-cc44-4a06-b8d4-7fe7a8f31a01" \
-H "Authorization: Bearer clapi_your_api_key"Task Status Example
curl "https://api.cleanlist.ai/api/v1/public/enrich/status?task_id=6aab5e77-f5c0-4233-8f8e-a0e1a9baf18c" \
-H "Authorization: Bearer clapi_your_api_key"Response (task):
{
"task": {
"task_id": "6aab5e77-f5c0-4233-8f8e-a0e1a9baf18c",
"status": "completed",
"message": "Enrichment completed"
}
}Webhooks
To receive webhook callbacks, pass webhook_url in your bulk enrichment request. The URL will receive a POST when the workflow completes.
List Delivery Attempts
GET /api/v1/public/webhooks/deliveries?workflow_id=<workflow_id>
curl "https://api.cleanlist.ai/api/v1/public/webhooks/deliveries?workflow_id=public_bulk_3d1f9a67-cc44-4a06-b8d4-7fe7a8f31a01" \
-H "Authorization: Bearer clapi_your_api_key"Polling Best Practice
Use backoff when polling status:
import time
import requests
def poll_workflow(workflow_id, api_key, max_wait=300):
url = "https://api.cleanlist.ai/api/v1/public/enrich/status"
headers = {"Authorization": f"Bearer {api_key}"}
delay = 1
elapsed = 0
while elapsed < max_wait:
response = requests.get(url, headers=headers, params={"workflow_id": workflow_id})
data = response.json()
workflow = data.get("workflow", {})
if workflow.get("status") in ("completed", "failed"):
return workflow
time.sleep(delay)
elapsed += delay
delay = min(delay * 2, 10)
raise TimeoutError(f"Workflow {workflow_id} did not complete within {max_wait}s")Rate Limits
Public endpoints documented on this page are limited to 60 requests per minute per organization in a fixed 60-second window.
If you exceed the limit, the API returns 429 Too Many Requests with a Retry-After header.
Example 429 response:
HTTP/1.1 429 Too Many Requests
Retry-After: 17
Content-Type: application/json
{
"detail": "Rate limit exceeded. Public API is limited to 60 requests per minute."
}