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.

API status v1 — stable. GA Base URL 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:

  1. Thread — a conversation. Every research task happens inside one.
  2. Message — a turn in the thread. When you send a user message, the planner immediately drafts a plan.
  3. Plan — a structured set of research steps produced by the LLM. Each plan is attached to a pending job.
  4. Job — the execution of an approved plan. Jobs stream events and produce an artifact.
  5. 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")
Prefer zero ceremony? Use 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

PrefixEnvironmentNotes
sk_live_…ProductionReal credits consumed. Do not commit to source control.
sk_test_…Test modeRuns the planner and returns plausible synthetic leads. No credits consumed.
pk_pub_…Client-sideRead-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:

readRead threads, plans, jobs, artifacts. Required for everything.
runCreate messages, approve jobs, stop jobs, run /search.
integrationsConnect CRMs, push results to external systems.
adminManage API keys, webhooks, team members, and usage limits.
Key safety Never expose 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/json for 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

400Malformed JSON or missing required parameters
401Missing, invalid, or revoked API key
402Out of credits or payment required
403Key lacks the required scope
404Resource not found (or not visible to your org)
409Illegal state transition (e.g. approving an already-running job)
422Semantically invalid (e.g. empty prompt, impossibly narrow ICP)
429Rate limit exceeded
500Unexpected server error. Retry with exponential backoff.
503Upstream research engine temporarily unavailable

Error codes

invalid_request_errorParameters failed validation
authentication_errorMissing, malformed, or revoked key
permission_errorKey is valid but lacks the scope
not_foundResource does not exist in this org
conflictIllegal state transition
rate_limit_errorToo many requests — back off and retry
insufficient_creditsNot enough credits to start a job
plan_rejectedThe planner declined the prompt (too vague, ungroundable, etc.)
job_haltedA step failed loudly — job cannot continue
integration_errorAn external CRM rejected the push
internal_errorUnexpected — 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)

ResourceStarterGrowthScale
Requests / minute606003,000
Concurrent jobs1525
Plans / day2002,000unlimited
SSE connections525250

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:

limit1–100, default 20
cursorPass the next_cursor from the previous response
orderasc 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.

POST /v1/threads Stable

Create a new thread. The thread is empty until you post a message.

Body parameters
FieldTypeDescription
titlestringOptional. Human-readable title (≤ 200 chars). Auto-generated from the first user message if omitted.
metadataobjectOptional. 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"}'
GET /v1/threads

List threads, most recent first.

Query parameters
limitinteger1–100. Default 20.
cursorstringPagination cursor.
qstringOptional title search.
has_job_statusstringFilter by last job status.
GET /v1/threads/{thread_id}

Retrieve a single thread.

PATCH /v1/threads/{thread_id}

Update a thread's title or metadata.

DELETE /v1/threads/{thread_id}

Delete a thread and all its messages, plans, jobs, and artifacts. Irreversible.

Running jobs If any job in the thread is still running, the delete returns 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.

POST /v1/threads/{thread_id}/messages

Append a user message and trigger the planner.

Body parameters
content*stringRequired. The user's prompt. 1–8,000 chars.
attachmentsarrayOptional. Array of { kind, url }. Supported kinds: csv (a seed list to enrich), url_list.
hintsobjectOptional soft constraints: { max_rows, preferred_sources }.
auto_approvebooleanOptional, 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_…" }
}
GET /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.

GET /v1/plans/{plan_id}

Retrieve a plan.

POST /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*stringRequired. What to change. Example: "Only VP Eng with Python experience. Skip Crunchbase."
The previous plan's pending job is cancelled and replaced with a new pending job. If the previous job was already running, stop it first or this call returns 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.

POST /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_urlstringOptional. Events for this job only are posted to this URL in addition to any organization-wide webhooks.
max_creditsintegerOptional ceiling. The job halts with status=stopped if it would exceed this.
POST /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.

GET /v1/jobs/{job_id}

Retrieve a job. Safe to poll — cheap, cacheable for 1 second.

GET /v1/jobs

List jobs across all threads in the org.

Query parameters
statusstringFilter by status. Comma-separate to combine.
thread_idstringOnly jobs in a specific thread.
created_aftertimestampISO-8601.
created_beforetimestampISO-8601.
GET /v1/jobs/{job_id}/logs

Return all log entries for a job — the same entries that were streamed via SSE during execution.

Streaming #

GET /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, or stopped.

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.

artifact

Emitted 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 #

GET /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.

GET /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
limitinteger1–100, default 50.
cursorstringPagination cursor.
min_confidencenumber0–1, default 0. Filter out weakly-grounded leads.
has_fieldstringRepeat to require multiple fields, e.g. has_field=person.email.
GET /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.

Integrations #

Push results directly to your CRM. Supported providers: hubspot, salesforce, pipedrive, attio, close, zoho, copper, google_sheets.

GET /v1/integrations

List connected integrations.

POST /v1/integrations/{provider}/connect

Initiate an OAuth connection. Returns a redirect URL for the user to complete authorization.

DELETE /v1/integrations/{provider}

Disconnect the integration.

POST /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*stringThe provider slug.
modestringcontacts (default), companies, or both.
list_idstringProvider-specific list or campaign ID. If omitted, a new list named after the thread title is created.
mappingobjectOptional column overrides.
dry_runbooleanDefault 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.

GET /v1/webhooks

List configured webhook endpoints.

POST /v1/webhooks

Register a new endpoint. Returns the signing_secret exactly once — store it securely.

Body parameters
url*stringHTTPS URL to deliver events to.
eventsarrayDefault ["*"]. Subscribe to specific event types.
descriptionstringFree-text label.
enabledbooleanDefault true.
PATCH /v1/webhooks/{webhook_id}

Update an endpoint's URL, events, description, or enabled state.

DELETE /v1/webhooks/{webhook_id}

Delete an endpoint.

POST /v1/webhooks/{webhook_id}/rotate

Rotate the signing secret.

POST /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

WorkloadTypical cost
Company discovery (no person)0.3–0.6 credits / row
Company + 1 person enriched1 credit / row
Company + 2–3 persons enriched1.5–2 credits / row
Deep enrich (hiring, tech stack, funding, news)+0.5 credits / row per dimension
Email verification+0.1 credits / email
GET /v1/usage

Usage summary for the current billing period.

Query parameters
fromdateInclusive lower bound (YYYY-MM-DD).
todateInclusive upper bound.
group_bystringday | 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 }
}
GET /v1/usage/budget

Return the organization's current spend ceiling and hard-stop threshold.

PUT /v1/usage/budget

Set a monthly budget ceiling. When reached, new jobs are rejected with insufficient_credits until the next cycle.

Account & keys #

GET /v1/me

Current authenticated principal (user + organization).

GET /v1/organization

Details for the key's organization.

GET /v1/api-keys

List API keys (metadata only — secret values are never returned).

POST /v1/api-keys

Mint a new API key. The secret is returned exactly once.

Body parameters
name*stringHuman label.
scopesarrayDefault ["read","run"].
expires_attimestampOptional auto-expiration.
DELETE /v1/api-keys/{key_id}

Revoke a key immediately.

GET /v1/members

List organization members (requires admin).

POST /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-EventEvent type, e.g. job.succeeded.
Leadex-DeliveryUnique delivery ID. Use for deduplication.
Leadex-TimestampUnix seconds when the delivery was signed.
Leadex-Signaturet=<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.completed

A 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.completed

A CRM push finished.

integration.push.failed

A CRM push errored out.

usage.budget.warning

Organization crossed 80% of its monthly budget.

usage.budget.exceeded

Budget 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 failed and shown in the dashboard.
  • Deliver responses are stored for 30 days at app.leadex.cc/settings/webhooks.
  • Use Leadex-Delivery to 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 #

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_credits on every approve / search call. Default to 1.5× the plan's credits_estimate.
  • Inspect the plan before approving. If steps.length > 8, call /plans/:id/replan with 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 plan12
Max rows in a single job's artifact10,000
Max attachments per message5
Max attachment size25 MB each
Max prompt length8,000 chars
Max webhook delivery body256 KB
Artifact download URL lifetime4 hours
SSE idle timeout10 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/search now accepts format=xlsx.
  • leads.delivered webhook event added — fires incrementally as leads are captured, not only at job end.

2026-03-28

  • SSE reconnection with Last-Event-ID now replays missed events.
  • Fix: job.stopped event was delivered twice in rare cases.

2026-03-06

  • New scopes: integrations separated from run. Existing keys auto-grandfathered with both.
  • /v1/integrations/attio added.

2026-02-11

  • Launch of /v1 stable. /v0 sunsets 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.

Glossary #

ArtifactThe deliverable of a job — a CSV file plus its metadata. One per job.
CreditUnit of usage metering. Roughly one enriched lead.
ICPIdeal Customer Profile — the audience description in a prompt.
JobThe execution of an approved plan.
LeadA structured record for a company and/or person, with source attribution.
PlanOrdered list of steps the planner produces from a prompt.
Research engineThe cloud subsystem that executes plan steps against the open web.
StepA single unit of work inside a plan (discover, enrich, verify, etc.).
ThreadA research conversation that hosts messages, plans, jobs, and artifacts.

Support #