Leadex API
Build prospecting into your product. The Leadex API lets you describe a target audience in plain English, have it drafted as a research plan, run it on the open web, and stream back enriched, deduplicated leads as CSV or JSON — all over a single REST surface.
https://api.leadex.cc/v1. All endpoints return JSON
unless otherwise noted. See versioning policy.
Mental model #
Everything in the API maps to five objects, in this order:
- Thread — a conversation. Every research task happens inside one.
- Message — a turn in the thread. When you send a user message, the planner immediately drafts a plan.
- Plan — a structured set of research steps produced by the LLM. Each plan is attached to a pending job.
- Job — the execution of an approved plan. Jobs stream events and produce an artifact.
- Artifact — the deliverable: one CSV per job, plus a JSON list of lead records.
This is the same flow the web app uses. Nothing the API does is hidden from the UI, and nothing the UI does is unavailable via the API.
Quickstart #
The snippet below creates a thread, sends a prompt, approves the plan, waits for completion, and downloads the CSV — end-to-end in under 60 lines.
# 1. Create a thread
THREAD=$(curl -sX POST https://api.leadex.cc/v1/threads \
-H "Authorization: Bearer $LEADEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Series A FinTech in EU"}' | jq -r .id)
# 2. Send the prompt — the planner runs automatically
JOB=$(curl -sX POST "https://api.leadex.cc/v1/threads/$THREAD/messages" \
-H "Authorization: Bearer $LEADEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content":"Find Series A fintechs in France with 20+ engineers. Pull VP Eng and Head of Growth."}' \
| jq -r .job.id)
# 3. Approve — kicks off research
curl -sX POST "https://api.leadex.cc/v1/jobs/$JOB/approve" \
-H "Authorization: Bearer $LEADEX_API_KEY"
# 4. Stream progress (or poll GET /jobs/$JOB)
curl -N "https://api.leadex.cc/v1/jobs/$JOB/stream" \
-H "Authorization: Bearer $LEADEX_API_KEY"
# 5. Download the CSV
ART=$(curl -s "https://api.leadex.cc/v1/jobs/$JOB" \
-H "Authorization: Bearer $LEADEX_API_KEY" | jq -r .artifact.id)
curl -sLo leads.csv "https://api.leadex.cc/v1/artifacts/$ART" \
-H "Authorization: Bearer $LEADEX_API_KEY"
import Leadex from "@leadex/sdk";
const leadex = new Leadex({ apiKey: process.env.LEADEX_API_KEY });
const thread = await leadex.threads.create({ title: "Series A FinTech in EU" });
const { job } = await leadex.messages.create(thread.id, {
content: "Find Series A fintechs in France with 20+ engineers. Pull VP Eng and Head of Growth.",
});
await leadex.jobs.approve(job.id);
for await (const event of leadex.jobs.stream(job.id)) {
if (event.type === "step") console.log(`[step] ${event.data.title}`);
if (event.type === "done") break;
if (event.type === "error") throw new Error(event.data.message);
}
const finished = await leadex.jobs.retrieve(job.id);
const csv = await leadex.artifacts.download(finished.artifact.id);
await csv.saveTo("./leads.csv");
from leadex import Leadex
client = Leadex() # reads LEADEX_API_KEY
thread = client.threads.create(title="Series A FinTech in EU")
msg = client.messages.create(
thread_id=thread.id,
content="Find Series A fintechs in France with 20+ engineers. Pull VP Eng and Head of Growth.",
)
client.jobs.approve(msg.job.id)
for event in client.jobs.stream(msg.job.id):
if event.type == "step": print("[step]", event.data["title"])
if event.type == "done": break
if event.type == "error": raise RuntimeError(event.data["message"])
job = client.jobs.retrieve(msg.job.id)
client.artifacts.download(job.artifact.id, to="./leads.csv")
POST /v1/search — a single call that wraps thread → plan → auto-approve → artifact. Good for one-off lookups.
Authentication #
The Leadex API authenticates with bearer tokens. Create and manage keys at app.leadex.cc/settings/api-keys. Keys are organization-scoped — every request runs as the organization the key belongs to.
Authorization: Bearer sk_live_5b9bfa7ab3fc10b981f59e0b…
Key prefixes
| Prefix | Environment | Notes |
|---|---|---|
sk_live_… | Production | Real credits consumed. Do not commit to source control. |
sk_test_… | Test mode | Runs the planner and returns plausible synthetic leads. No credits consumed. |
pk_pub_… | Client-side | Read-only. Safe to embed in browsers. Can only list public artifacts the org has marked shareable. |
Scopes
Secret keys carry one or more of the following scopes:
read | Read threads, plans, jobs, artifacts. Required for everything. |
run | Create messages, approve jobs, stop jobs, run /search. |
integrations | Connect CRMs, push results to external systems. |
admin | Manage API keys, webhooks, team members, and usage limits. |
sk_live_… keys in client-side code. If a key is leaked, rotate it immediately from the dashboard — old keys are revoked on rotation and all in-flight jobs continue with the new key automatically.
Base URL & versioning #
All requests are sent to:
https://api.leadex.cc/v1
The version is embedded in the path. We guarantee backwards compatibility within a major version: no endpoint is removed, no field is renamed, no required field is added. Additive changes (new endpoints, new optional fields, new event types) may land at any time — make sure your clients ignore unknown fields.
When a breaking change is on the horizon, we publish /v2 in parallel and keep /v1 online for at least 12 months with a public deprecation notice at the top of this page and a Sunset header on all responses.
Request / response #
- Content type. Requests and responses are JSON. Use
Content-Type: application/jsonfor writes. - Timestamps. ISO 8601, UTC, millisecond precision:
2026-04-21T14:22:01.430Z. - IDs. Opaque prefixed strings — treat them as strings, not integers. Prefixes:
thr_,msg_,pln_,job_,art_,lead_,whk_,evt_. - Nullable fields. Optional fields are either present or
null— never missing. - Money & credits. Credits are integers (fractional usage is rounded up at the end of a job).
- Unknown fields. Always ignore what you don't recognize — we add new fields in a backwards-compatible way.
Errors #
Every error response shares the same envelope:
{
"error": {
"code": "invalid_request_error",
"message": "Thread thr_42 was not found in your organization.",
"param": "thread_id",
"request_id": "req_01HX8Z9YQ0M3P4"
}
}
Status codes
400 | Malformed JSON or missing required parameters |
401 | Missing, invalid, or revoked API key |
402 | Out of credits or payment required |
403 | Key lacks the required scope |
404 | Resource not found (or not visible to your org) |
409 | Illegal state transition (e.g. approving an already-running job) |
422 | Semantically invalid (e.g. empty prompt, impossibly narrow ICP) |
429 | Rate limit exceeded |
500 | Unexpected server error. Retry with exponential backoff. |
503 | Upstream research engine temporarily unavailable |
Error codes
invalid_request_error | Parameters failed validation |
authentication_error | Missing, malformed, or revoked key |
permission_error | Key is valid but lacks the scope |
not_found | Resource does not exist in this org |
conflict | Illegal state transition |
rate_limit_error | Too many requests — back off and retry |
insufficient_credits | Not enough credits to start a job |
plan_rejected | The planner declined the prompt (too vague, ungroundable, etc.) |
job_halted | A step failed loudly — job cannot continue |
integration_error | An external CRM rejected the push |
internal_error | Unexpected — always includes a request_id |
Every error includes a request_id. Include it when contacting support@leadex.cc — we can find the full trace in seconds with it.
Rate limits #
Limits are per-organization and tier-dependent. Every response includes:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1745231000
Retry-After: 12 # only on 429
Default limits (Starter / Growth / Scale)
| Resource | Starter | Growth | Scale |
|---|---|---|---|
| Requests / minute | 60 | 600 | 3,000 |
| Concurrent jobs | 1 | 5 | 25 |
| Plans / day | 200 | 2,000 | unlimited |
| SSE connections | 5 | 25 | 250 |
Need more? Contact sales@leadex.cc — limits on Scale are soft and can be raised on request.
Idempotency #
All write endpoints accept an optional Idempotency-Key header (any string ≤ 255 chars). We cache the response for 24 hours — a duplicate request with the same key returns the original response and does not perform the action twice.
POST /v1/jobs/job_01HX9/approve
Idempotency-Key: approve-job_01HX9-attempt-3
Using a key on read endpoints has no effect. Keys are scoped to your organization. A key reused with a different request body returns 409 conflict.
Pagination #
List endpoints are cursor-paginated. Query parameters:
limit | 1–100, default 20 |
cursor | Pass the next_cursor from the previous response |
order | asc or desc (default desc) |
{
"data": [ /* array of resource objects */ ],
"has_more": true,
"next_cursor": "c_01HX9ZA…",
"total": null
}
Request IDs & tracing #
Every response includes X-Request-Id. If you send your own X-Request-Id header (any UUID), we log and echo it back — useful for tying your application traces to ours.
Core objects #
Below is the canonical shape for each object. These are the exact payloads returned everywhere they appear — in responses, SSE events, and webhook deliveries. Any additional fields we introduce later are additive.
The thread object #
{
"id": "thr_01HXA4Z2M8V5WC9T7R1K",
"object": "thread",
"title": "Series A FinTech in EU",
"created_at": "2026-04-21T10:11:32.200Z",
"updated_at": "2026-04-21T10:14:02.081Z",
"message_count": 4,
"last_job_id": "job_01HXA4ZR…",
"metadata": { "crm_campaign_id": "camp_117" }
}
metadata is a free-form object of up to 50 key/value pairs (string keys ≤ 40 chars, string values ≤ 500 chars). Useful for joining Leadex IDs to objects in your own system.
The message object #
{
"id": "msg_01HXA5…",
"object": "message",
"thread_id": "thr_01HXA4Z…",
"role": "user",
"kind": "text",
"content": "Find Series A fintechs in France…",
"plan_id": null,
"job_id": null,
"created_at":"2026-04-21T10:11:32.500Z"
}
The plan object #
{
"id": "pln_01HXA5…",
"object": "plan",
"job_id": "job_01HXA5…",
"title": "Series A FinTech in France — VP Eng & Head of Growth",
"steps": [
{
"id": "stp_1",
"title": "Discover Series A fintechs in France",
"intent":"discover",
"task": "Filter by industry=fintech, country=FR, stage=series_a, last 36 months. Capture company_name, domain, hq_city, funding_amount, funded_at.",
"expected_columns": ["company_name","domain","hq_city","funding_amount","funded_at"]
}
],
"credits_estimate": 45,
"created_at": "2026-04-21T10:11:34.010Z"
}
The job object #
{
"id": "job_01HXA5…",
"object": "job",
"thread_id": "thr_01HXA4…",
"plan_id": "pln_01HXA5…",
"status": "succeeded",
"progress": { "step": 5, "total": 5, "percent": 100 },
"started_at": "2026-04-21T10:12:01.000Z",
"finished_at": "2026-04-21T10:19:44.300Z",
"credits_used": 42,
"artifact": { "id": "art_01HXA6…", "rows": 83, "bytes": 14220 },
"summary": "Found 83 Series A fintechs in France with ≥20 engineers.",
"error": null,
"metadata": {}
}
The artifact object #
{
"id": "art_01HXA6…",
"object": "artifact",
"job_id": "job_01HXA5…",
"format": "csv",
"rows": 83,
"bytes": 14220,
"columns": ["company_name","domain","vp_eng_name","vp_eng_email"],
"download_url":"https://artifacts.leadex.cc/art_01HXA6.../leads.csv?token=…",
"expires_at": "2026-04-21T14:19:44.300Z",
"created_at": "2026-04-21T10:19:44.300Z"
}
download_url is a short-lived signed URL (valid 4 hours). Call GET /v1/artifacts/{id} any time to mint a fresh one.
The lead object #
{
"id": "lead_01HXA6…",
"object": "lead",
"job_id": "job_01HXA5…",
"company": { "name": "Spendesk", "domain": "spendesk.com", "linkedin": "…", "hq_city": "Paris" },
"person": { "full_name": "Jordan Lacan", "title": "VP of Engineering", "email": "j.lacan@spendesk.com" },
"fields": { "engineering_headcount": 64, "funding_amount": "€100M" },
"sources": [
{ "kind": "web", "url": "https://crunchbase.com/organization/spendesk", "retrieved_at": "2026-04-21T10:13:42.001Z" }
],
"confidence": 0.93
}
Every field in fields traces back to at least one entry in sources. If a fact can't be grounded, it's omitted rather than guessed. confidence is a 0–1 score combining source strength and cross-source agreement.
Threads #
A thread is a single research conversation. Threads persist indefinitely and can host any number of plans and jobs. In the web app, each row in the left rail is a thread.
/v1/threads
Stable
Create a new thread. The thread is empty until you post a message.
Body parameters
| Field | Type | Description |
|---|---|---|
title | string | Optional. Human-readable title (≤ 200 chars). Auto-generated from the first user message if omitted. |
metadata | object | Optional. Up to 50 key/value pairs for your own joining keys. |
Response
Returns a thread object.
curl -X POST https://api.leadex.cc/v1/threads \
-H "Authorization: Bearer $LEADEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Series A FinTech in EU"}'
/v1/threads
List threads, most recent first.
Query parameters
limit | integer | 1–100. Default 20. |
cursor | string | Pagination cursor. |
q | string | Optional title search. |
has_job_status | string | Filter by last job status. |
/v1/threads/{thread_id}
Retrieve a single thread.
/v1/threads/{thread_id}
Update a thread's title or metadata.
/v1/threads/{thread_id}
Delete a thread and all its messages, plans, jobs, and artifacts. Irreversible.
409 conflict. Stop running jobs first, or pass ?force=true to stop them as part of the delete.
Messages #
Posting a user message is how you start research. Every user message is sent to the planner together with the full thread history — the planner then produces a plan attached to a new pending job. Nothing runs until you approve.
/v1/threads/{thread_id}/messages
Append a user message and trigger the planner.
Body parameters
content* | string | Required. The user's prompt. 1–8,000 chars. |
attachments | array | Optional. Array of { kind, url }. Supported kinds: csv (a seed list to enrich), url_list. |
hints | object | Optional soft constraints: { max_rows, preferred_sources }. |
auto_approve | boolean | Optional, default false. If true, the job runs immediately. |
Response
{
"message": { "id": "msg_…", "role": "user", "content": "…" },
"plan": { "id": "pln_…", "steps": [ … ], "credits_estimate": 45 },
"job": { "id": "job_…", "status": "pending", "plan_id": "pln_…" }
}
/v1/threads/{thread_id}/messages
List messages in a thread, chronologically.
Plans #
Plans are produced automatically when a user message is posted. You normally don't create them directly — instead you fetch, inspect, or ask for a replan.
/v1/plans/{plan_id}
Retrieve a plan.
/v1/plans/{plan_id}/replan
Ask the planner to produce a new plan for the same thread with corrective feedback. Equivalent to the user sending another message in the thread, but scoped specifically as "redo the previous plan."
Body parameters
feedback* | string | Required. What to change. Example: "Only VP Eng with Python experience. Skip Crunchbase." |
409 conflict.
Jobs #
A job is the execution of an approved plan. It moves through pending → running → succeeded | failed | stopped. One CSV artifact is produced per successful job.
/v1/jobs/{job_id}/approve
Approve a pending job and start execution. Research begins immediately; the response returns once the job has been scheduled (typically < 200ms).
Body parameters
webhook_url | string | Optional. Events for this job only are posted to this URL in addition to any organization-wide webhooks. |
max_credits | integer | Optional ceiling. The job halts with status=stopped if it would exceed this. |
/v1/jobs/{job_id}/stop
Stop a running job. The server sets a stop flag; the current step is aborted at the next checkpoint and the job transitions to stopped. Any rows collected up to the stop are written to a partial artifact.
/v1/jobs/{job_id}
Retrieve a job. Safe to poll — cheap, cacheable for 1 second.
/v1/jobs
List jobs across all threads in the org.
Query parameters
status | string | Filter by status. Comma-separate to combine. |
thread_id | string | Only jobs in a specific thread. |
created_after | timestamp | ISO-8601. |
created_before | timestamp | ISO-8601. |
/v1/jobs/{job_id}/logs
Return all log entries for a job — the same entries that were streamed via SSE during execution.
Streaming #
/v1/jobs/{job_id}/stream
SSE
Open a Server-Sent Events stream for a job. Receives events in real time while the job runs; closes cleanly when the job terminates.
Connection details
- Protocol: SSE. No WebSocket.
- Heartbeat every 20s as an SSE comment (
:ping). - Reconnect by re-issuing the request with
Last-Event-ID. - Stream auto-closes on
done,error, orstopped.
Event types
log Free-text progress line. Low-signal; useful for a live feed.
step Step lifecycle: { step_id, status: "started"|"completed"|"failed" }.
result A batch of one or more lead objects as they are captured.
artifactEmitted once the CSV is ready: { artifact_id, rows, bytes }.
error A failure that halts the job.
done Terminal success. Includes final job payload and summary.
Example SSE stream
id: evt_1
event: step
data: {"step_id":"stp_1","status":"started","title":"Discover Series A fintechs"}
id: evt_2
event: log
data: {"message":"Captured 34 candidate companies"}
id: evt_99
event: done
data: {"job_id":"job_…","artifact_id":"art_…","summary":"Found 83 companies…"}
Artifacts & leads #
/v1/artifacts/{artifact_id}
Fetch the artifact record — including a fresh signed download_url valid for 4 hours. Pass ?format=csv|json|xlsx for an alternate format; we render on demand and cache.
/v1/jobs/{job_id}/leads
List the leads produced by a job, paginated. Richer than the CSV — includes per-field sources and confidence.
Query parameters
limit | integer | 1–100, default 50. |
cursor | string | Pagination cursor. |
min_confidence | number | 0–1, default 0. Filter out weakly-grounded leads. |
has_field | string | Repeat to require multiple fields, e.g. has_field=person.email. |
/v1/jobs/{job_id}/summary
Retrieve the LLM-written summary that accompanies the result message in the web app. Includes row count, coverage notes, and any caveats.
Search Beta #
A convenience wrapper around the entire flow. Gives you a one-shot endpoint that takes a prompt and returns leads — no thread, no approval step, no event stream required.
/v1/search
Run an audience query end-to-end and return leads inline.
Body parameters
prompt* | string | The audience description in plain English. |
max_rows | integer | Cap on leads returned. 1–2,000. Default 200. |
max_credits | integer | Credit ceiling. |
wait | string | sync (default) returns when done. async returns a job_id immediately. |
format | string | json (default) or csv. |
Example
curl -X POST https://api.leadex.cc/v1/search \
-H "Authorization: Bearer $LEADEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "CTOs of US Series B dev-tools companies with open JD for Staff Engineer",
"max_rows": 150
}'
/search uses the same planner, executor, and dedupe pipeline as the interactive flow. The only difference is that the plan is auto-approved and the response blocks (or returns a job pointer) instead of appearing as a chat turn.
Integrations #
Push results directly to your CRM. Supported providers: hubspot, salesforce, pipedrive, attio, close, zoho, copper, google_sheets.
/v1/integrations
List connected integrations.
/v1/integrations/{provider}/connect
Initiate an OAuth connection. Returns a redirect URL for the user to complete authorization.
/v1/integrations/{provider}
Disconnect the integration.
/v1/jobs/{job_id}/push
Push the leads from a succeeded job to a connected integration. Idempotent on (job_id, provider, mode).
Body parameters
provider* | string | The provider slug. |
mode | string | contacts (default), companies, or both. |
list_id | string | Provider-specific list or campaign ID. If omitted, a new list named after the thread title is created. |
mapping | object | Optional column overrides. |
dry_run | boolean | Default false. If true, returns the planned mutations without applying them. |
Response
{
"push_id": "psh_01HXA7…",
"provider": "hubspot",
"created": 72,
"updated": 11,
"skipped": 0,
"errors": [],
"list_id": "482911",
"list_url": "https://app.hubspot.com/contacts/42/lists/482911"
}
Webhook endpoints #
Manage the URLs that receive webhook deliveries. See events & payloads below.
/v1/webhooks
List configured webhook endpoints.
/v1/webhooks
Register a new endpoint. Returns the signing_secret exactly once — store it securely.
Body parameters
url* | string | HTTPS URL to deliver events to. |
events | array | Default ["*"]. Subscribe to specific event types. |
description | string | Free-text label. |
enabled | boolean | Default true. |
/v1/webhooks/{webhook_id}
Update an endpoint's URL, events, description, or enabled state.
/v1/webhooks/{webhook_id}
Delete an endpoint.
/v1/webhooks/{webhook_id}/rotate
Rotate the signing secret.
/v1/webhooks/{webhook_id}/test
Send a test payload to the endpoint.
Usage & credits #
Every job consumes credits. A credit roughly corresponds to one enriched lead; plans publish an estimate before you approve and a final number on the finished job. Credits are consumed only on successful or stopped jobs — failed jobs refund automatically.
Credit cost by workload
| Workload | Typical cost |
|---|---|
| Company discovery (no person) | 0.3–0.6 credits / row |
| Company + 1 person enriched | 1 credit / row |
| Company + 2–3 persons enriched | 1.5–2 credits / row |
| Deep enrich (hiring, tech stack, funding, news) | +0.5 credits / row per dimension |
| Email verification | +0.1 credits / email |
/v1/usage
Usage summary for the current billing period.
Query parameters
from | date | Inclusive lower bound (YYYY-MM-DD). |
to | date | Inclusive upper bound. |
group_by | string | day | thread | user. |
{
"period": { "from": "2026-04-01", "to": "2026-04-21" },
"plan": "growth",
"credits": { "included": 5000, "used": 3127, "remaining": 1873, "rollover": 0 },
"jobs": { "succeeded": 41, "failed": 2, "stopped": 1 }
}
/v1/usage/budget
Return the organization's current spend ceiling and hard-stop threshold.
/v1/usage/budget
Set a monthly budget ceiling. When reached, new jobs are rejected with insufficient_credits until the next cycle.
Account & keys #
/v1/me
Current authenticated principal (user + organization).
/v1/organization
Details for the key's organization.
/v1/api-keys
List API keys (metadata only — secret values are never returned).
/v1/api-keys
Mint a new API key. The secret is returned exactly once.
Body parameters
name* | string | Human label. |
scopes | array | Default ["read","run"]. |
expires_at | timestamp | Optional auto-expiration. |
/v1/api-keys/{key_id}
Revoke a key immediately.
/v1/members
List organization members (requires admin).
/v1/members
Invite a user by email.
Webhooks — signing #
Every delivery carries an HMAC-SHA256 signature over the raw request body. Verify it before trusting the payload.
Headers
Leadex-Event | Event type, e.g. job.succeeded. |
Leadex-Delivery | Unique delivery ID. Use for deduplication. |
Leadex-Timestamp | Unix seconds when the delivery was signed. |
Leadex-Signature | t=<ts>,v1=<hmac>. Multiple v1= values during secret rotation — accept either. |
Verification (Node)
import crypto from "node:crypto";
function verify(req, secret) {
const header = req.headers["leadex-signature"];
const body = req.rawBody;
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const signed = `${parts.t}.${body.toString("utf8")}`;
const mac = crypto.createHmac("sha256", secret).update(signed).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now()/1000 - Number(parts.t)) < 300;
return ok && fresh;
}
Webhooks — event catalog #
thread.created A new thread was created.
thread.deleted A thread and its descendants were deleted.
message.created A new user or assistant message was appended.
plan.created A plan was drafted and attached to a pending job.
plan.rejected The planner declined the prompt.
job.approved A pending job was approved.
job.started The runner has begun executing the first step.
job.step.started A step moved from pending to running.
job.step.completedA step finished successfully.
job.step.failed A step halted with an error.
job.progress Throttled (every 10%) progress update.
job.succeeded Job completed; artifact is ready.
job.failed Job halted with an error; any partial artifact is attached.
job.stopped Job was stopped by the user or by a budget ceiling.
leads.delivered Leads were produced and are now queryable.
artifact.created A CSV artifact is ready to download.
integration.push.completedA CRM push finished.
integration.push.failedA CRM push errored out.
usage.budget.warningOrganization crossed 80% of its monthly budget.
usage.budget.exceededBudget ceiling was hit; new jobs are blocked.
api_key.rotated A key was rotated.
Delivery payload shape
{
"id": "evt_01HXA7Z…",
"type": "job.succeeded",
"created_at": "2026-04-21T10:19:44.300Z",
"livemode": true,
"data": {
"object": { /* full job, artifact, thread, etc. depending on type */ }
}
}
Webhooks — retries #
- Any non-2xx response (or timeout > 10 s) triggers a retry.
- Retry schedule: 5s, 30s, 2min, 10min, 1h, 6h, 24h (max 24h & 9 attempts).
- After the final attempt, the delivery is marked
failedand shown in the dashboard. - Deliver responses are stored for 30 days at app.leadex.cc/settings/webhooks.
- Use
Leadex-Deliveryto deduplicate — at-least-once delivery is the only guarantee.
Node / TypeScript SDK #
Official SDK, generated from the OpenAPI spec and hand-wrapped for ergonomics.
npm install @leadex/sdk
# or
pnpm add @leadex/sdk
yarn add @leadex/sdk
Example
import Leadex from "@leadex/sdk";
const leadex = new Leadex({
apiKey: process.env.LEADEX_API_KEY!,
baseURL: "https://api.leadex.cc/v1",
maxRetries: 3,
timeout: 30_000,
});
const job = await leadex.search({
prompt: "US Series B dev-tools CTOs hiring Staff Engineers",
max_rows: 100,
});
console.log(job.leads.length, "leads");
Streaming helper
for await (const event of leadex.jobs.stream(jobId)) {
switch (event.type) {
case "log": progress.setLabel(event.data.message); break;
case "step": progress.tick(event.data.step_id); break;
case "result": store.addLeads(event.data.leads); break;
case "done": return;
}
}
Source & issues: github.com/leadex/sdk-node.
Python SDK #
pip install leadex
Example
from leadex import Leadex
client = Leadex()
job = client.search(
prompt="US Series B dev-tools CTOs hiring Staff Engineers",
max_rows=100,
)
print(len(job.leads), "leads")
# Async variant
import asyncio
from leadex import AsyncLeadex
async def main():
async with AsyncLeadex() as client:
async for event in client.jobs.stream(job_id):
if event.type == "result":
print("got", len(event.data.leads), "leads")
asyncio.run(main())
Source & issues: github.com/leadex/sdk-python. Community SDKs exist for Go, Ruby, and Elixir — see the Leadex GitHub org.
Recipes #
Small end-to-end snippets for the most common integration shapes. Each assumes LEADEX_API_KEY is set and uses the Node SDK for brevity — the shapes translate 1-to-1 to Python or raw curl.
1 · Natural-language ICP → CSV
Single prompt, 500-row list. The "hello world" of Leadex.
2 · Enrich a list of domains
Attach seed CSV; the planner enriches instead of discovering.
3 · Auto-sync to HubSpot
Webhook on job.succeeded → /jobs/:id/push.
4 · Weekly prospecting cron
Schedule a server job to run /search every Monday.
5 · Slack slash-command bot
/leads <prompt> posts a CSV back to the channel.
6 · Custom UI on top of Leadex
Proxy SSE through your backend; render plan approvals in your app.
7 · Cancel a runaway job
Watchdog that stops anything past 10 min or 500 credits.
8 · Programmatic replanning
Generate → inspect → refine, without a human in the loop.
9 · Compare two ICP cohorts
Two /search calls; diff by domain.
10 · Cost-control patterns
Budget ceilings, dry-run plans, test-mode keys.
1 · Natural-language ICP → CSV #
const job = await leadex.search({
prompt: "Heads of Data at US healthcare companies, 500–5000 employees, using Snowflake",
max_rows: 500,
format: "csv",
});
await fs.writeFile("heads-of-data.csv", job.csv);
2 · Enrich a list of domains #
const thread = await leadex.threads.create({ title: "Enrichment — April batch" });
const { job } = await leadex.messages.create(thread.id, {
content: "For each company in the attached CSV, pull the CTO and Head of Engineering. Add funding stage, last-raised date, and tech stack.",
attachments: [{ kind: "csv", url: "https://ops.example.com/seeds.csv" }],
});
await leadex.jobs.approve(job.id);
3 · Auto-sync to HubSpot #
app.post("/leadex/hook", verifySignature, async (req, res) => {
if (req.body.type !== "job.succeeded") return res.sendStatus(204);
const jobId = req.body.data.object.id;
await leadex.jobs.push(jobId, {
provider: "hubspot",
mode: "both",
list_id: process.env.HS_INBOX_LIST,
});
res.sendStatus(200);
});
4 · Weekly prospecting cron #
// Runs every Monday 09:00 UTC.
await leadex.search({
prompt: "New Series A US SaaS companies announced in the last 7 days, hiring for Head of Marketing",
max_rows: 200,
wait: "async",
});
5 · Slack slash-command bot #
app.post("/slack/leads", async (req, res) => {
const { text, response_url } = req.body;
res.json({ response_type: "in_channel", text: `Researching: _${text}_…` });
const job = await leadex.search({ prompt: text, max_rows: 100, wait: "sync" });
await fetch(response_url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Done — ${job.leads.length} leads. <${job.artifact.download_url}|Download CSV>`,
}),
});
});
6 · Custom UI on top of Leadex #
Proxy the SSE stream through your own backend so you never expose API keys to the browser.
app.get("/api/jobs/:id/stream", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
const upstream = await leadex.jobs.streamRaw(req.params.id);
upstream.pipe(res);
});
7 · Cancel a runaway job #
setInterval(async () => {
const running = await leadex.jobs.list({ status: "running" });
for (const j of running.data) {
const age = Date.now() - Date.parse(j.started_at);
const burn = j.credits_used ?? 0;
if (age > 10 * 60_000 || burn > 500) {
await leadex.jobs.stop(j.id, { reason: "watchdog: budget/time exceeded" });
}
}
}, 30_000);
8 · Programmatic replanning #
let { plan, job } = await leadex.messages.create(threadId, { content: initialPrompt });
while (plan.credits_estimate > 100) {
const feedback = `Trim to under 100 credits. Drop the most expensive step.`;
({ plan, job } = await leadex.plans.replan(plan.id, { feedback }));
}
await leadex.jobs.approve(job.id);
9 · Compare two ICP cohorts #
const [a, b] = await Promise.all([
leadex.search({ prompt: "Series B AI-infra companies in US hiring ML engineers", max_rows: 300 }),
leadex.search({ prompt: "Series B AI-infra companies in EU hiring ML engineers", max_rows: 300 }),
]);
const domains = new Set(a.leads.map(l => l.company.domain));
const overlap = b.leads.filter(l => domains.has(l.company.domain));
console.log(`US: ${a.leads.length}, EU: ${b.leads.length}, overlap: ${overlap.length}`);
10 · Cost-control patterns #
- Use
sk_test_…keys in CI — plans run but execution is synthetic and free. - Set
max_creditson everyapprove/searchcall. Default to 1.5× the plan'scredits_estimate. - Inspect the plan before approving. If
steps.length > 8, call/plans/:id/replanwith stricter feedback. - Subscribe to
usage.budget.warning— route to Slack before the hard stop. - Set a hard monthly ceiling with
PUT /v1/usage/budget.
Limits & policies #
Hard caps
| Max steps in a plan | 12 |
| Max rows in a single job's artifact | 10,000 |
| Max attachments per message | 5 |
| Max attachment size | 25 MB each |
| Max prompt length | 8,000 chars |
| Max webhook delivery body | 256 KB |
| Artifact download URL lifetime | 4 hours |
| SSE idle timeout | 10 minutes without events |
Data retention
- Threads, messages, plans, jobs, and leads: retained until deleted.
- Artifacts (CSV blobs): retained for 90 days after the parent job finishes, then purged. Lead JSON remains indefinitely.
- Raw sources used to ground leads: retained for 30 days for audit, then purged.
- Webhook delivery logs: 30 days.
- Deletion of a thread cascades to all descendants within 24 hours.
PII & compliance
- Leadex only surfaces data that is publicly available on the open web.
- GDPR requests (access / erasure) are processed within 30 days. Contact privacy@leadex.cc.
- SOC 2 Type II report available on request under NDA.
Acceptable use
Do not use the API to harass individuals, build HR blocklists, or target protected classes. Violating accounts are suspended without refund. See the full terms of service.
Changelog #
2026-04-14 New
POST /v1/searchnow acceptsformat=xlsx.leads.deliveredwebhook event added — fires incrementally as leads are captured, not only at job end.
2026-03-28
- SSE reconnection with
Last-Event-IDnow replays missed events. - Fix:
job.stoppedevent was delivered twice in rare cases.
2026-03-06
- New scopes:
integrationsseparated fromrun. Existing keys auto-grandfathered with both. /v1/integrations/attioadded.
2026-02-11
- Launch of
/v1stable./v0sunsets on 2027-02-11.
OpenAPI spec #
The machine-readable source of truth. Use it to generate custom SDKs, import into Postman, or wire up API explorers.
openapi.yaml— OpenAPI 3.1 (YAML)- Postman collection
Glossary #
| Artifact | The deliverable of a job — a CSV file plus its metadata. One per job. |
| Credit | Unit of usage metering. Roughly one enriched lead. |
| ICP | Ideal Customer Profile — the audience description in a prompt. |
| Job | The execution of an approved plan. |
| Lead | A structured record for a company and/or person, with source attribution. |
| Plan | Ordered list of steps the planner produces from a prompt. |
| Research engine | The cloud subsystem that executes plan steps against the open web. |
| Step | A single unit of work inside a plan (discover, enrich, verify, etc.). |
| Thread | A research conversation that hosts messages, plans, jobs, and artifacts. |
Support #
- Status page: status.leadex.cc
- Email: support@leadex.cc — include a
request_idwhere possible. - Security & disclosure: security@leadex.cc.
- Feature requests & bug reports: github.com/leadex/api-issues.