API reference
Find, verify, qualify and research B2B contacts from your code. Same coin balance as the dashboard, same waterfall under the hood, JSON in / JSON out.
Quick start
Three steps from zero to your first lookup:
- Generate a key at Settings → API Keys. Use a test key (
ml_test_…) to integrate without burning credits. - Send the key as a Bearer token. Send
Idempotency-Key too — your retries become safe. - Hit any
POST /v1/... endpoint. Get JSON back. Done.
First request
curl -X POST https://app.modernleads.io/v1/lookup \
-H "Authorization: Bearer ml_live_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"first_name": "John",
"last_name": "Doe",
"domain": "acme.com"
}'
Authentication
Every request must include Authorization: Bearer ml_live_… (production) or Bearer ml_test_… (sandbox). Test keys never burn credits and never call providers — perfect for integration testing.
Keys are scoped. By default a new key gets every scope; restrict on creation if your integration only needs one (e.g. verifyonly). Calling an endpoint outside your key's scopes returns 403 scope_denied.
Available scopes
lookup POST /v1/lookup
verify POST /v1/verify/email, /v1/batch/verify
qualify POST /v1/qualify
domain_finder POST /v1/domain-finder
research POST /v1/research/prospect
batch POST /v1/batch/* + GET /v1/batch/*
clean_names POST /v1/batch/clean-names
Rate limits
60 requests per minute per key by default. Bursts up to 120/min for one minute per hour. Hourly safety cap on credits applies to API calls just like the dashboard.
When you hit the limit you get 429 with a Retry-Afterseconds header. Back off + retry with jitter — never tight-loop a 429.
Idempotency
Send Idempotency-Key: <uuid> on every state-mutating call. If your retry hits us with the same key + same payload within 24 hours, you get the original response back (with header X-Idempotent-Replay: true). No double charges, no duplicate batches.
If the key is reused with a different payload, we return 409 idempotency_key_payload_mismatch — generate a fresh UUID instead.
Error responses
Every error response is JSON with the shape:
{
"error": "Human-readable summary",
"code": "machine_readable_code",
"message": "Optional longer message",
"cta_url": "https://app.modernleads.io/coins",
"cta_label": "Top up"
}
| Status | Code | Meaning |
|---|
| 400 | invalid_input | Body validation failed |
| 401 | missing_key | invalid_key | revoked_key | expired_key | Auth failure |
| 402 | insufficient_credits | Top up at /coins |
| 403 | subscription_required | scope_denied | ip_denied | Upgrade or check key permissions |
| 404 | not_found | Batch / resource doesn't exist for this key |
| 409 | idempotency_key_payload_mismatch | buyer_context_missing | See message |
| 429 | rate_limited | spend_cap_reached | Back off + retry with jitter |
| 500 | internal | Our problem. Retry — Idempotency-Key keeps it safe |
Endpoints
Body
first_namestring · required | Contact's first name |
last_namestring · required | Contact's last name |
domainstring · either | Company domain (preferred) |
linkedin_urlstring · either | LinkedIn profile URL |
lookup_typeenum | "email" (default), "phone", or "both" |
company_namestring | Improves accuracy |
job_titlestring | Improves accuracy |
countrystring | ISO country code |
Request
curl -X POST https://app.modernleads.io/v1/lookup \
-H "Authorization: Bearer ml_live_your_key" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"first_name":"John","last_name":"Doe","domain":"acme.com","lookup_type":"both"}'
Response 200
{
"email": "john@acme.com",
"phone": "+91 99999 00000",
"status": "found",
"verified": true,
"credits_charged": 6,
"cached_hit": false,
"alt_emails": ["john.doe@acme.com"],
"alt_phones": []
}
Body
emailstring · required | Email to verify |
Request
curl -X POST https://app.modernleads.io/v1/verify/email \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"email":"john@acme.com"}'
Response 200
{
"email": "john@acme.com",
"status": "valid",
"verified": true,
"deliverable": true,
"cached": false,
"credits_charged": 1
}
Body
domainstring · required | Prospect company domain |
company_namestring | Improves scoring |
first_namestring | Person mode (with last_name) |
last_namestring | Person mode |
job_titlestring | Person mode |
employeesnumber | Company mode |
industrystring | Company mode |
descriptionstring | Company mode |
Request
curl -X POST https://app.modernleads.io/v1/qualify \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"domain":"acme.com","company_name":"Acme","employees":50,"industry":"SaaS"}'
Response 200
{
"fit_score": 8,
"fit_label": "high",
"reason": "Mid-market SaaS in your ICP, 50-employee sweet spot.",
"credits_charged": 1
}
Body
company_namestring · required | Company name to resolve |
Request
curl -X POST https://app.modernleads.io/v1/domain-finder \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"company_name":"Modern Inbound"}'
Response 200
{
"company_name": "Modern Inbound",
"domain": "acme.com",
"confidence": "high",
"credits_charged": 1
}
POST/v1/research/prospect10 credits
Full SDR research dossier (sync, ~30–60s)
Body
domainstring · required | Prospect domain |
company_namestring | Display name |
contact_namestring | Decision-maker name |
contact_titlestring | Decision-maker title |
company_descriptionstring | Hint to the LLM |
Request
curl -X POST https://app.modernleads.io/v1/research/prospect \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"domain":"acme.com","contact_name":"John Doe","contact_title":"VP Sales"}'
Response 200
{
"job_id": "uuid",
"output": {
"prospect_company": "Acme",
"fit_assessment": { "fit_score": 8, "fit_label": "high", "reason": "..." },
"why_now": { "signal_found": true, "signal": "...", "source": "..." },
"connection_point": { "detail": "...", "usable_in_email": "..." }
},
"credits_charged": 10
}
POST/v1/batch/enrich1–6 credits per row
Bulk enrich up to 10,000 contacts. Async + webhook.
Body
rowsarray · required | Array of {first_name, last_name, domain | linkedin_url, ...} |
find_phonesboolean | Skip phone-finding to save coins. Default true. |
namestring | Job display name |
Request
curl -X POST https://app.modernleads.io/v1/batch/enrich \
-H "Authorization: Bearer ml_live_your_key" \
-d '{
"name": "Q2 outbound",
"find_phones": true,
"rows": [
{"first_name":"John","last_name":"Doe","domain":"acme.com"},
{"first_name":"Jane","last_name":"Smith","linkedin_url":"https://linkedin.com/in/janesmith"}
]
}'
Response 200
{
"batch_id": "uuid",
"status": "queued",
"total_rows": 2,
"status_url": "https://app.modernleads.io/v1/batch/uuid",
"download_url": "https://app.modernleads.io/v1/batch/uuid/download"
}
POST/v1/batch/verify0.1 credits per email
Bulk verify up to 10,000 emails. Async + webhook.
Body
emailsstring[] · required | Array of email addresses |
namestring | Job display name |
Request
curl -X POST https://app.modernleads.io/v1/batch/verify \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"emails":["a@b.com","c@d.com","e@f.com"]}'
Response 200
{
"batch_id": "uuid",
"status": "queued",
"total_emails": 3,
"status_url": "https://app.modernleads.io/v1/batch/uuid"
}
Body
namesstring[] · required | Company names to normalise |
Request
curl -X POST https://app.modernleads.io/v1/batch/clean-names \
-H "Authorization: Bearer ml_live_your_key" \
-d '{"names":["Acme Inc.","Foo Pvt Ltd","Bar GmbH"]}'
Response 200
{
"cleaned": ["Acme", "Foo", "Bar"]
}
GET/v1/batch/{id}Free
Status of an async batch
Request
curl https://app.modernleads.io/v1/batch/uuid \
-H "Authorization: Bearer ml_live_your_key"
Response 200
{
"batch_id": "uuid",
"kind": "enrich",
"status": "processing",
"name": "Q2 outbound",
"total_rows": 1000,
"processed_rows": 423,
"found_count": 312,
"not_found_count":111,
"download_url": null
}
Request
curl https://app.modernleads.io/v1/batch/uuid/download \
-H "Authorization: Bearer ml_live_your_key"
Response 200
{
"batch_id": "uuid",
"results": [
{
"first_name": "John", "last_name": "Doe", "domain": "acme.com",
"email": "john@acme.com", "phone": "+1 555 010 0001",
"status": "found", "verified": true
},
...
]
}
Receiving webhook events
Async batches notify your server when they complete. Register a URL at Settings → API Keys → Webhook endpoints. You'll get a whsec_… secret (shown once) for HMAC verification.
Event payload
POST https://your-server.com/webhooks/modernleads
Content-Type: application/json
X-ModernLeads-Signature: <hex hmac-sha256>
X-ModernLeads-Timestamp: <unix seconds>
X-ModernLeads-Event: batch.completed
X-ModernLeads-Delivery: <uuid>
{
"event": "batch.completed",
"delivery_id": "uuid",
"data": {
"batch_id": "uuid",
"kind": "enrich",
"total": 1000,
"found": 842,
"download_url": "https://app.modernleads.io/v1/batch/uuid/download",
"completed_at": "2026-04-26T10:34:21Z"
}
}
Failed deliveries retry at 1m, 5m, 30m, 6h. After that the delivery moves to your DLQ (visible in the dashboard) for manual replay.
Verifying webhook signatures
Always verify the HMAC signature + timestamp before trusting a payload. The signature is computed as hmac_sha256(secret, "{timestamp}.{rawBody}").
Verification
import crypto from "crypto";
export function verifyModernLeadsWebhook(
rawBody: string,
headers: Record<string, string>,
secret: string,
): boolean {
const sig = headers["x-modernleads-signature"];
const ts = headers["x-modernleads-timestamp"];
if (!sig || !ts) return false;
// Anti-replay: reject if timestamp is more than 5 min off.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
MCP server (Claude / Cursor / Windsurf)
Talk to Modern Leads in natural language from any Model Context Protocol client. Two install options: clone-and-build (works today) or one-line npx (once published to npm).
Option 1 — clone & build (2 minutes)
git clone https://github.com/grooveclassic2310/modern-leads.git
cd modern-leads/mcp
npm install && npm run build
Then add to your client's MCP config (Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json):
Claude Code / Claude Desktop config
{
"mcpServers": {
"modernleads": {
"command": "node",
"args": ["/absolute/path/to/modern-leads/mcp/dist/index.js"],
"env": {
"MODERNLEADS_API_KEY": "ml_live_your_key"
}
}
}
}
Restart your MCP client. Ten Modern Leads tools (lookup, verify, qualify, domain-finder, research, batch_*) appear in the tool list and your client can call them naturally:
- “Find the verified email for John Doe at acme.com.”
- “Verify these 200 emails and tell me which ones bounce.”
- “Score these 50 companies against my ICP and dump the top 10 to a CSV.”
- “Run SDR research on stripe.com — pull a fit assessment + a connection point.”
Source code: github.com/grooveclassic2310/modern-leads/tree/main/mcp