Public REST API
Drive Enrichabl from your own code. Create pipelines, push leads, run AI enrichment and email validation, and pull results - all over a simple JSON API.
https://api.enrichabl.com/v1JSON over HTTPSBearer token auth600 req / min / tokenQuickstart
From zero to your first enriched lead in five steps.
- 1Generate a tokenOn your profile page, scroll to Personal API Tokens, click Generate token, and copy the value. We don't store the plaintext, so this is your one chance to see it.
- 2Add provider API keys (if using AI)For AI / multi-AI columns, add the corresponding provider key (OpenAI, Gemini, etc.) under Settings → API Keys. Email validation needs BounceBan and/or Mailveri. Firecrawl needs a Firecrawl key.
- 3Create a pipeline
POST /v1/pipelineswith a name. You'll get apipeline_idyou'll use everywhere else. - 4Upload leads
POST /v1/pipelines/:id/leadswith a JSON array. Onlyemailis required. Up to ~5,000 leads per request (4 MB body cap), max 3 concurrent uploads per account. - 5Enrich and pull results
POST /v1/enrichment/startkicks off a background job. Poll/v1/enrichment/jobs/:job_id/progressuntilCOMPLETED, then read enriched values back viaGET /v1/pipelines/:id/leads.
# Hello-world: list your pipelines
curl https://api.enrichabl.com/v1/pipelines \
-H "Authorization: Bearer eab_live_..."Authentication
Every /v1/* request requires a Personal Access Token in the Authorization header.
Authorization: Bearer eab_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxToken lifecycle
- Tokens are shown once, on creation. We store only a SHA-256 hash, so a lost token cannot be recovered - revoke and regenerate.
- Revoking a token takes effect immediately - the next request returns
401. - The first 16 characters (e.g.
eab_live_ab12cd34) are stored and shown in the UI so you can identify a token without revealing the secret. last_used_atupdates on every successful request.- Tokens carry the same permissions as your account. There are no read-only or scoped tokens in v1.
Rate limits & quotas
| Limit | Value | When you'll hit it |
|---|---|---|
| Requests / token | 600 / minute | Tight loops; chunked exports of millions of rows. Returns 429. |
| Body size / request | 4 MB | ~5,000 typical leads per upload. Chunk larger lists. |
| Concurrent bulk uploads | 3 per account | Beyond that → 429 until one finishes. |
| Lead listing page size | 10,000 max (default 1,000) | Use offset for the next page. |
| Lead quota - Free | 100 leads | Upload past it → 402. |
| Lead quota - Starter | 30,000 leads | Same. |
| Lead quota - Pro | Unlimited (1M hard ceiling) | For all practical purposes, unlimited. |
Quotas count across all your pipelines, not per pipeline. There is no per-pipeline cap.
Errors
Errors are JSON: {"error": "Human-readable message"}. Status codes:
| Code | Meaning |
|---|---|
| 400 | Invalid body, missing required field, unknown enum value |
| 401 | Missing, malformed, or revoked token |
| 402 | Lead quota exceeded - body includes current_count / quota_limit |
| 403 | Subscription gate (read-only mode, missing provider API key, etc.) |
| 404 | Resource doesn't exist or isn't owned by your token's user |
| 429 | Rate-limit or concurrent-bulk-ops cap |
| 500 | Server error - retry with exponential backoff |
Pipelines
Create a pipeline
POST /v1/pipelines
curl -X POST https://api.enrichabl.com/v1/pipelines \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pipeline_name": "Q3 outbound",
"pipeline_description": "SaaS leads from Apollo",
"pipeline_color": "#8B5CF6"
}'Required: pipeline_name. Optional: pipeline_description, pipeline_color. Returns the full pipeline including pipeline_id.
List your pipelines
GET /v1/pipelines
curl https://api.enrichabl.com/v1/pipelines -H "Authorization: Bearer $TOKEN"Each pipeline is returned with computed stats: total_leads, imported_leads, validated_leads, enriched_leads, valid_emails, etc.
Get a single pipeline
GET /v1/pipelines/:id
Leads
Upload leads
POST /v1/pipelines/:id/leads
Body: a JSON array. email is the only required field. custom_fields is a free-form object that's stored verbatim on each lead.
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/leads \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '[
{
"email": "ada@example.com",
"first_name": "Ada",
"last_name": "Lovelace",
"company": "Analytical Engines",
"title": "Founder",
"linkedin_url": "https://linkedin.com/in/adalovelace",
"company_website": "https://analyticalengines.com",
"custom_fields": { "source": "Apollo", "tier": "A" }
},
{ "email": "alan@bletchley.test" }
]'Supported fields
| Field | Notes |
|---|---|
| email * | Required. Used for dedup. |
| first_name, last_name | String |
| company, title, phone | String |
| linkedin_url, company_website | String. Used by Firecrawl as url_column source. |
| company_description, company_size, industry, location | String |
| custom_fields | Object. Stored as JSONB. AI/Firecrawl outputs are also written here. |
Behaviour
- Response code is
202 Acceptedwith a summary ofcreated/skipped/failedper row. - Dedup is by email, scoped to your account. Re-uploading an email that already exists in any of your pipelines is silently skipped - not an error.
- The handler enforces your subscription quota atomically (advisory lock). If a batch would exceed the cap, it's truncated to fit, and the response reports how many were dropped.
- Two batches uploaded simultaneously by the same user are serialized at the quota check, so you can't race past the limit.
429).List leads (paginated)
GET /v1/pipelines/:id/leads?limit=1000&offset=0
curl "https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/leads?limit=10000&offset=0" \
-H "Authorization: Bearer $TOKEN"{
"data": [
{
"pipeline_lead_id": "…",
"pipeline_lead_stage": "IMPORTED",
"pipeline_lead_email_validation_status": "VALID",
"lead": {
"lead_id": "…",
"lead_email": "ada@example.com",
"lead_company": "Analytical Engines",
"lead_custom_fields": { "source": "Apollo", "tier": "A" },
"lead_enrichment_status": "ENRICHED"
}
}
],
"pagination": {
"limit": 10000,
"offset": 0,
"total": 23104,
"has_more": true
}
}limit- clamped to[1, 10000]. Default 1000.offset- must be ≥ 0. Default 0.- Result is sorted by
pipeline_lead_created_at DESCwith a stable tiebreaker onpipeline_lead_id, so pages don't repeat or skip rows even after rapid bulk inserts. - Each row is a
pipeline_leadsjunction record with the underlying lead object nested underlead.
Columns
Columns control what shows up alongside each lead in the UI and drive what enrichment writes back. There are four kinds.
| Type | Use for |
|---|---|
base | A lead field already on the record (e.g. industry, company_size). |
ai | A single-value AI-generated column (one prompt → one output). |
multi_ai | A multi-field AI column (one prompt → many output fields). |
firecrawl | Web-scraped data from a URL on each lead. |
All four use the same endpoint:
POST /v1/pipelines/:id/columns
Base columns
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/columns \
-H "Authorization: Bearer $TOKEN" \
-d '{ "type": "base", "key": "industry" }'AI columns
You can either reference an existing template by ID, or create one inline.
# Inline (creates the template + attaches it in one call)
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/columns \
-H "Authorization: Bearer $TOKEN" \
-d '{
"type": "ai",
"template": {
"name": "Company summary",
"prompt_template": "Summarize {{company}} in one sentence.",
"model": "gpt-4o-mini",
"max_tokens": 200
}
}'
# Or reference an existing template
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/columns \
-d '{ "type": "ai", "template_id": "<ai_column_template_id>" }'gpt-4o-mini → OpenAI. Pass gemini-2.5-flash → Gemini. Pass sonar-pro → Perplexity. You only need to set endpoint_url manually for custom OpenAI-compatible deployments.Supported models
| Provider | Models |
|---|---|
| OpenAI | gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano, o1, o1-mini, o3, o3-mini, o4-mini |
| Gemini | gemini-2.5-pro, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.0-flash, gemini-2.0-flash-lite |
| Perplexity | sonar, sonar-pro, sonar-reasoning, sonar-reasoning-pro |
gemini-2.0-flash-exp and other variants are not in the allow-list and will be rejected. Use gemini-2.0-flash.Multi-AI columns
Multi-AI templates produce several fields from one prompt, parsed as JSON. Create the template first via the dedicated endpoint, then attach a column per output field.
# 1. Create the multi-AI template
curl -X POST https://api.enrichabl.com/v1/columns/multi-ai-templates \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "CompanyProfile",
"output_fields": ["headquarters_city", "founded_year", "industry"],
"prompt_template": "For {{company}}, return JSON: {\"headquarters_city\":\"…\",\"founded_year\":\"…\",\"industry\":\"…\"}",
"model": "gpt-4o",
"max_tokens": 300
}'
# Response: { "multi_ai_id": "...", ... }
# 2. Attach a column per output field
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/columns \
-d '{ "type": "multi_ai", "template_id": "<multi_ai_id>", "field": "headquarters_city" }'Inline multi-AI templates aren't supported - always create the template first, then attach.
Firecrawl columns
Firecrawl scrapes a URL on each lead and uses its built-in LLM to extract structured fields. Create a template (so the column shows up in the UI), then run enrichment with per-job extraction config.
url_column is the lead-field suffix only. Pass "company_website", not "lead_company_website". It also matches keys inside lead_custom_fields if your URL is stored there.# 1. Create the template (saves it for UI display)
curl -X POST https://api.enrichabl.com/v1/columns/firecrawl-templates \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "WebExtract",
"url_column": "company_website",
"prompt": "Extract the tagline and main product from this company website.",
"output_fields": ["tagline", "main_product"]
}'
# Response: { "firecrawl_id": "...", ... }
# 2. Attach a column per output field
curl -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/columns \
-d '{ "type": "firecrawl", "template_id": "<firecrawl_id>", "field": "tagline" }'Where enriched values land
Every enriched value is stored on the lead inside lead_custom_fields (read it back from GET /v1/pipelines/:id/leads, under lead.lead_custom_fields). The key naming follows a strict convention:
| Column type | Custom-field key |
|---|---|
ai | {template_name} |
multi_ai | {template_name}: {field} |
firecrawl | Firecrawl: {field} |
"Firecrawl: ", not your template name. If you create a Firecrawl template called "WebExtract" with output field tagline, the value lands at lead_custom_fields["Firecrawl: tagline"] - not lead_custom_fields["WebExtract: tagline"].Enrichment
Run enrichment on the whole pipeline, the top N most-recent leads, or a specific list.
Three ways to target leads
# Whole pipeline
curl -X POST https://api.enrichabl.com/v1/enrichment/start \
-H "Authorization: Bearer $TOKEN" \
-d '{ "pipeline_id": "'$PIPELINE_ID'", "enrichment_type": "AI_COLUMN", "ai_template_id": "..." }'
# Top 100 most-recent leads in the pipeline
curl -X POST https://api.enrichabl.com/v1/enrichment/start \
-d '{ "pipeline_id": "'$PIPELINE_ID'", "limit": 100, "enrichment_type": "AI_COLUMN", "ai_template_id": "..." }'
# Specific leads
curl -X POST https://api.enrichabl.com/v1/enrichment/start \
-d '{ "lead_ids": ["<id1>", "<id2>"], "enrichment_type": "AI_COLUMN", "ai_template_id": "..." }'enrichment_type is uppercase. Use AI_COLUMN, MULTI_AI_COLUMN, or FIRECRAWL_EXTRACT. Lowercase values are rejected with {"error":"invalid enrichment_type"}.Per-type body shapes
AI_COLUMN
{
"pipeline_id": "...",
"enrichment_type": "AI_COLUMN",
"ai_template_id": "<ai_column_template_id>"
}MULTI_AI_COLUMN
{
"pipeline_id": "...",
"enrichment_type": "MULTI_AI_COLUMN",
"multi_ai_template_id": "<multi_ai_id>"
}FIRECRAWL_EXTRACT
The extraction config goes in the request body, not the template. The template only exists for UI display.
{
"pipeline_id": "...",
"enrichment_type": "FIRECRAWL_EXTRACT",
"firecrawl_extract_config": {
"url_column": "company_website",
"prompt": "Extract the tagline and main product.",
"output_fields": ["tagline", "main_product"]
}
}Tracking jobs
# Poll progress
curl https://api.enrichabl.com/v1/enrichment/jobs/$JOB_ID/progress \
-H "Authorization: Bearer $TOKEN"
# Cancel
curl -X POST https://api.enrichabl.com/v1/enrichment/jobs/$JOB_ID/cancel \
-H "Authorization: Bearer $TOKEN"Progress response includes:
status-PENDING,IN_PROGRESS,COMPLETED,FAILED,CANCELLEDprogress_percentage,processed_leads,total_leads,failed_leadscompleted_lead_ids/failed_lead_idslead_errors- per-lead error messages on failed rows
400 with a clear error like "No API key found for OPENAI". Add the key, then retry.Email validation
Same target modes as enrichment: whole pipeline, top N, or specific lead IDs.
# Whole pipeline
curl -X POST https://api.enrichabl.com/v1/validation/start \
-H "Authorization: Bearer $TOKEN" \
-d '{ "pipeline_id": "'$PIPELINE_ID'", "service": "BOUNCEBAN" }'
# Top 50, with Mailveri
curl -X POST https://api.enrichabl.com/v1/validation/start \
-d '{ "pipeline_id": "'$PIPELINE_ID'", "limit": 50, "service": "MAILVERI" }'
# Specific leads, with Hybrid (BounceBan + Mailveri)
curl -X POST https://api.enrichabl.com/v1/validation/start \
-d '{ "lead_ids": ["<id>"], "service": "HYBRID" }'
# Poll / cancel
curl https://api.enrichabl.com/v1/validation/jobs/$JOB_ID/progress -H "Authorization: Bearer $TOKEN"
curl -X POST https://api.enrichabl.com/v1/validation/jobs/$JOB_ID/cancel -H "Authorization: Bearer $TOKEN"Services
| service | Notes |
|---|---|
| BOUNCEBAN | Real-time SMTP probing. Fast on corporate domains, often returns UNKNOWN for Gmail/Yahoo (their MX servers block probes). |
| MAILVERI | Heuristic + reputation. Generally faster end-to-end than BounceBan. |
| HYBRID | BounceBan first; falls back to Mailveri on inconclusive results. Requires both API keys. |
Result statuses
Read each lead's outcome from pipeline_lead_email_validation_status: VALID, INVALID, RISKY, or UNKNOWN.
processed_leads counter only counts the new work. To force re-validation, you'd need to clear the status (UI flow today; not exposed in the v1 API).valid_leads / invalid_leads / risky_leads) only tally definitive outcomes. Leads that come back UNKNOWN are counted in processed_leads but not in V/I/R, so a job can show 100% processed with V+I+R less than total.Cookbook
Upload 20,000 leads, then enrich the freshest 1,000
# Chunk in batches of 4,000 (each ~700 KB; well under the 4 MB cap)
for offset in 0 4000 8000 12000 16000; do
jq -n --argjson o $offset '[range($o; $o+4000) | {email: "user\(.)@stresstest.example"}]' \
| curl -s -X POST https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/leads \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" --data-binary @-
done
# Run AI enrichment on just the 1,000 most-recent leads
curl -X POST https://api.enrichabl.com/v1/enrichment/start \
-H "Authorization: Bearer $TOKEN" \
-d '{ "pipeline_id": "'$PIPELINE_ID'", "limit": 1000,
"enrichment_type": "AI_COLUMN", "ai_template_id": "..." }'Export every lead in a pipeline as JSON
offset=0
while : ; do
page=$(curl -s "https://api.enrichabl.com/v1/pipelines/$PIPELINE_ID/leads?limit=10000&offset=$offset" \
-H "Authorization: Bearer $TOKEN")
jq -c '.data[]' <<< "$page" >> all-leads.ndjson
more=$(jq -r '.pagination.has_more' <<< "$page")
[ "$more" = "true" ] || break
offset=$((offset + 10000))
donePagination is stable across rapid bulk inserts (we tiebreak by pipeline_lead_id), so you won't see duplicate or skipped rows even if leads were uploaded milliseconds apart.
Wait for an enrichment job to finish
while : ; do
resp=$(curl -s https://api.enrichabl.com/v1/enrichment/jobs/$JOB_ID/progress \
-H "Authorization: Bearer $TOKEN")
status=$(jq -r .status <<< "$resp")
jq -r '"\(.status) \(.processed_leads)/\(.total_leads)"' <<< "$resp"
case "$status" in COMPLETED|FAILED|CANCELLED) break ;; esac
sleep 5
doneVersioning
The API is versioned by URL prefix: /v1/*. Backwards-incompatible changes ship under /v2/* with a deprecation window during which both run in parallel.
Not in v1 - planned
- Webhooks for job completion (today: poll
/jobs/:id/progress). - Token scopes (read-only / write).
- Cursor-based pagination for very large pipelines.
- Inline multi-AI / Firecrawl template creation in
POST /v1/pipelines/:id/columns. - Force re-validation flag on
POST /v1/validation/start.
Endpoint summary
| Method | Path | Purpose |
|---|---|---|
| GET | /v1/pipelines | List pipelines (with stats) |
| POST | /v1/pipelines | Create a pipeline |
| GET | /v1/pipelines/:id | Get one pipeline |
| POST | /v1/pipelines/:id/leads | Upload leads (array) |
| GET | /v1/pipelines/:id/leads | List leads (paginated) |
| POST | /v1/pipelines/:id/columns | Add a column |
| GET | /v1/columns/ai-templates | List AI templates |
| POST | /v1/columns/ai-templates | Create an AI template |
| GET | /v1/columns/multi-ai-templates | List multi-AI templates |
| POST | /v1/columns/multi-ai-templates | Create a multi-AI template |
| GET | /v1/columns/firecrawl-templates | List Firecrawl templates |
| POST | /v1/columns/firecrawl-templates | Create a Firecrawl template |
| POST | /v1/enrichment/start | Start an enrichment job |
| GET | /v1/enrichment/jobs/:job_id | Get job record |
| GET | /v1/enrichment/jobs/:job_id/progress | Poll progress |
| POST | /v1/enrichment/jobs/:job_id/cancel | Cancel a running job |
| POST | /v1/validation/start | Start a validation job |
| GET | /v1/validation/jobs/:id/progress | Poll progress |
| POST | /v1/validation/jobs/:id/cancel | Cancel a running job |
Need help? Email alex@code2b.co. Found a bug or undocumented behaviour? Please tell us, accuracy of these docs matters.