Reference

API documentation

A REST API for finding a person’s work email from their name and company domain, and verifying it against the receiving mail server before you send. Every response is JSON. The base URL is https://www.findleademails.com.

Authentication

Create a key under Developers. It is shown once and stored only as a SHA-256 hash, so it cannot be recovered later — generate a new one if you lose it. Pass it on every request:

curl https://www.findleademails.com/api/v1/email/find \
  -H "x-api-key: fle_live_..." \
  -H "content-type: application/json" \
  -d '{"first_name":"Sarah","last_name":"Jones","company_domain":"example.com"}'

A Bearer token is accepted in place of the header. Requests without a valid key return 401.

MCP (for AI agents)

https://www.findleademails.com/api/mcp is a Streamable HTTP MCP server exposing the same finder and jobs functionality below as tools — for an MCP-compatible agent (Claude Code, Claude Desktop, and others) to call directly, no REST client code to write.

Authenticate the same way as the REST API — an API key as a Bearer token, added to your client’s MCP config:

{
  "mcpServers": {
    "findleademails": {
      "url": "https://www.findleademails.com/api/mcp",
      "headers": { "Authorization": "Bearer fle_live_..." }
    }
  }
}
ToolEquivalent to
find_emailPOST /api/v1/email/find
find_emails_bulkPOST /api/v1/bulk/find
create_jobPOST /api/v1/jobs
get_job_statusGET /api/v1/jobs/{id}
get_job_resultsGET /api/v1/jobs/{id}/results

Billing matches the REST API below: 1 credit for a verified result, and catch-alls, misses, and repeat lookups are free. A failed tool returns { error, code, ... }. Copy-paste setup for Cursor, Claude Desktop, Claude Code, and VS Code is in the setup guide, including the codes this server returns.

Find one email

POST /api/v1/email/find

{
  "email": "sarah.jones@example.com",
  "status": "verified",
  "verification_status": "valid",
  "confidence": 0.99,
  "confidence_label": "VERY_HIGH",
  "needs_verification": false,
  "credits_used": 1,
  "credits_remaining": 942
}

status is one of verified (the mail server accepted the mailbox — safe to send), catch_all (the domain accepts every address, so nothing can be confirmed and a prediction is returned), predicted (no verdict available), or not_found (every candidate was rejected).

Stable fields are email, status, confidence, needs_verification, credits_used and credits_remaining. Other fields may be added over time.

No company website? Leave company_domain empty and send company_linkedin_url instead (linkedin.com/company/…) — it’s resolved to a real domain first, then looked up as normal. It’s a fallback, not an override: if both are given, company_domain is used as-is. That resolution is billed separately, 1 extra credit, only when it finds a match; a genuine no-match costs nothing and returns a 400.

company_linkedin_url is unrelated to linkedin_url — that field is the person’s own profile, saved on the result for reference and never used to drive the lookup. A personal profile URL isn’t accepted as company_linkedin_url either — resolving a person’s current employer from their own profile is a different, unsupported problem.

Find in bulk

POST /api/v1/bulk/find takes a records array and answers in the same request. It is meant for interactive use and is capped per request; oversized batches return 413 with code: "batch_too_large" and the limit in max_rows.

Rows are grouped by domain, so a company’s convention is resolved once and reused for everyone at it. For anything larger, use jobs.

A row can leave company_domain empty and send company_linkedin_url instead per row here too — see above. A row that fails to resolve comes back as invalid_input rather than failing the whole batch.

Rate limits

POST /api/v1/email/find and POST /api/v1/bulk/find are rate limited per workspace (keyed by your API key), scaled to your plan:

PlanPer secondPer day
Launch33,000
Growth615,000
Scale1245,000
Volume30300,000

Buying credits without a subscription gets you the same limits as the plan carrying that many credits — a 50,000-credit balance is treated as Volume — so a pack is always spendable at the rate you bought it. If you are on a plan and also hold credits, you get whichever of the two is higher, never the lower.

Every response — success or error — carries your current headroom as headers:

x-ratelimit-limit-second: 6
x-ratelimit-remaining-second: 5
x-ratelimit-limit-day: 15000
x-ratelimit-remaining-day: 14992

Exceeding either window returns 429 with code: "rate_limited", a Retry-After header in seconds, and the same delay in the body as retry_after_seconds.

Integration guidance: back off for at least retry_after_seconds and retry once — don’t loop retrying immediately, and don’t treat a 429 as a failed lookup (nothing is charged for it). If you’re driving lookups from a platform like Clay, n8n, or your own bulk enrichment job, the robust pattern is to read x-ratelimit-remaining-second on every response and pace the next request to stay under it, rather than relying on 429s as the throttle. Async jobs below are not subject to this limit — they have their own per-workspace queue-depth cap instead.

Jobs (async)

POST /api/v1/jobs accepts up to 50,000 records, returns immediately with 202, and runs off-request.

curl https://www.findleademails.com/api/v1/jobs \
  -H "x-api-key: fle_live_..." \
  -H "content-type: application/json" \
  -H "Idempotency-Key: your-unique-id" \
  -d '{"records":[{"first_name":"Sarah","last_name":"Jones","company_domain":"example.com"}],
       "callback_url":"https://your-app.example/hooks"}'

Send an Idempotency-Key so a retry returns the original job instead of creating and charging a second one.

Poll GET /api/v1/jobs/{id} for counters, and page results with GET /api/v1/jobs/{id}/results?cursor=0 until next_cursor comes back null. Results are readable while the job is still running. DELETE /api/v1/jobs/{id} cancels one.

If you set callback_url, completion POSTs a job.completed payload signed as x-fle-signature: sha256=<hmac> over the raw body. Verify it before trusting the payload. Delivery is attempted once and a failure never fails the job, so treat polling as the reliable path.

Inbound webhooks

Everything above expects you to send our field names and put a key in a header. Plenty of tools can do neither — a list-builder’s “export to webhook” posts its own column names and attaches no headers at all. For those, create an inbound webhook under Developers and give the tool the URL you get.

POST https://www.findleademails.com/api/webhooks/ingest/whk_live_…

The token sits in the URL because those senders have nowhere else to put it, so it is a narrower credential than an API key on purpose: it can submit rows and nothing else — it cannot read results or touch your workspace. Revoking it takes effect immediately. Where a tool can set a header, prefer an API key and /api/v1/email/find.

You do not have to match our field names. domain, website, firstName, company and the other common spellings are understood. A single flat object, a bare array, or a list under records / rows / leads all work, up to 50 rows per request.

No domain column? Map a company_linkedin_url column instead and leave the domain field out — see Find one email. It costs 1 extra credit, only when it resolves; a row that doesn’t counts toward skipped in the summary below, free.

The same fields can go in the query string instead of the body, which is where Clay and Airscale both put a “Query params” section — the body is optional if the query carries everything. Where both supply a field the body wins, so adding a query param can never quietly override what an existing integration already sends.

POST /api/webhooks/ingest/whk_live_…?first_name=Ada&last_name=Lovelace&domain=example.com

A row that already carries a usable address is handed straight back and costs nothing — paying to re-derive an address you already hold would be absurd. An email field that is not shaped like an address (an empty string, or the literal "not found", which some tools send) counts as no email rather than as one.

Add ?verified_only=true and anything we could not confirm against the mail server comes back empty instead — including an address you supplied, since we never checked it. Use it when the response feeds a column you will later mail-merge, so that column only ever holds confirmed addresses.

Add ?format=text and you get the address as a bare string rather than a JSON document — for a tool that drops the whole response body into one cell. With no address to give, the body is the row’s status (not_found, catch_all) rather than a phrase like “not found”: that value lands in a column something will treat as an address, and a status cannot be mistaken for one.

{
  "email":  "sarah.jones@acme.com",
  "status": "verified",
  "received": 1,
  "found": 1,
  "already_had_email": 0,
  "skipped": 0,
  "credits_used": 1,
  "results": [ … ]
}

email and status appear at the top level when exactly one row was posted — which is how most of these tools call us — so mapping the answer into a single column does not mean reaching into results[0]. Billing and error codes are identical to the rest of the API.

Rate limiting differs in one way, deliberately. These senders fire a burst and map each reply into a column, so instead of returning 429 an inbound webhook waits for a slot and then runs the row — you get every answer rather than losing the overflow. The work is still paced to your plan, so a burst takes longer rather than failing. A spent daily allowance is still refused immediately, since waiting cannot bring it back, and the synchronous endpoints above are unchanged.

What you are charged

You are billed only for a result you can use. A verified result costs 1 credit. catch_all is free — the domain accepts every address, so no specific mailbox can actually be confirmed, and charging for a result we ourselves label unconfirmed would contradict this promise. not_found is free too, and so is a repeat lookup of someone already found. A predicted result is free whenever verification ran but could not reach a verdict.

Credits never expire, and they survive downgrade and cancellation.

Errors

Errors carry an HTTP status and, where it helps, a stable machine-readable code.

401  Unauthorized            missing or revoked API key
402  insufficient_credits    top up, or choose a larger plan
413  batch_too_large         split the batch; see max_rows
413  job_too_large           more than 50,000 records in one job
429  rate_limited            back off for retry_after_seconds; see Rate limits
429  too_many_queued_jobs    let a running job finish first
503  jobs_unavailable        async jobs are not configured

A 5xx is safe to retry. For job creation, retry with the same Idempotency-Key so you are never charged twice.

Need a higher volume rate?

If you are embedding this in a product or reselling enrichment, the per-credit rate drops well below the published plans.

hi@findleademails.com