Find customers and email them — from your code or your AI agent.
The LeadGen.tools API finds real businesses, their contacts and decision makers, verifies emails, and runs cold email campaigns with automatic follow-ups and AI reply detection. It is the same engine as the web app, billed from the same credit balance.
Base URL
https://mail.leadgen.tools/v2/api/v1/Every endpoint is a .php file under this URL, for example https://mail.leadgen.tools/v2/api/v1/companies.php. Use HTTPS.
Requests
- GET endpoints take their parameters in the query string.
- POST / PUT endpoints take a JSON body with
Content-Type: application/json. companies.phpandpeople.phpaccept both: query string with GET, or a JSON body with POST.api_keyalways goes in the query string, on every call — also on POST and PUT.
Responses
Every response is JSON (Content-Type: application/json; charset=utf-8) with the same envelope:
{
"status": "success",
"credits_used": 4,
"data": { }
}{
"status": "error",
"code": "insufficient_credits",
"message": "You have no credits remaining. Current balance: 0"
}credits_used is what that call took from your balance. 0 means nothing billable was delivered, the answer came from the cache, or you had already paid for those items. Errors carry a machine-readable code and a readable message — see Errors.
All endpoints
| Endpoint | Method | What it does | Credits |
|---|---|---|---|
| companies.php | GET · POST | Businesses for a niche + city, optionally with emails | 2 / page, +1 / +2 per email |
| people.php | GET · POST | Decision makers at a domain, ranked for your offer | 2 / person with email, 1 without |
| search.php | GET | Leads from search engine results | 0–10 |
| crawl.php | GET | Emails, phones and social links from a website | 1 per valid email |
| ads.php | GET | Companies running search ads, with contact data | 0–10 |
| places/search.php | GET | Raw maps listings | 2 / page |
| places/reviews.php | GET | Reviews of a place | 0 or 1 |
| verify.php | POST | Verify up to 20 emails | 0 |
| projects.php | GET · POST | List / create projects | 0 |
| leads.php | GET · POST | List / add leads in a project | 0 |
| accounts.php | GET | Connected sending accounts | 0 |
| campaigns.php | GET · POST | List, read, create campaigns | 0 |
| campaigns/generate.php | POST | Write a sequence with AI | 1 per email (2–6) |
| campaigns/steps.php | PUT | Replace a campaign's emails | 0 |
| campaigns/launch.php | POST | Launch a draft campaign | 0 (sending: 1 / prospect) |
| campaigns/control.php | POST | Pause, resume or cancel | 0 |
| campaigns/status.php | GET | Campaign progress | 0 |
| campaigns/recipients.php | GET | Recipients, replies and their category | 0 |
Get your API key
Your API key is the license of your LeadGen.tools account. It identifies the account, so every call works on your data and spends your credits.
- Sign in to LeadGen.toolsOpen the app with your account (or create a free one).
- Open “API & Agents”In the sidebar, open Advanced tools → API & Agents. Go there now →
- Copy the keyClick Reveal or Copy. Your licenses are also listed at leadgen.tools/members/softsale/license.
- Send it as the
api_keyquery-string parameter on every request. - API calls spend the same credit balance as the web app. Top up from your account.
- In a team, only the account owner and admins can see the key in the app.
- No key listed? The API & Agents page tells you how to get one, or write to us.
Quickstart
Your first call in about a minute: 20 dental clinics in Austin, TX.
- Save your key in an environment variable
Terminal
export LEADGEN_API_KEY="your_api_key" - Ask for companiesThe request shown here returns one page of ~20 real businesses with website, phone, address, rating and coordinates. It costs 2 credits.
- Add emailsAdd
with_emails=trueto read each company's website for an email (+1 credit per email found, +2 when it comes verified from our data partners). - Keep goingPass
next_page_tokenback aspage_tokenfor more, then find the decision makers and launch a campaign.
curl -G "https://mail.leadgen.tools/v2/api/v1/companies.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "query=dental clinics" \
--data-urlencode "location=Austin, TX" \
--data-urlencode "limit=20"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
query: "dental clinics",
location: "Austin, TX",
limit: 20
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/companies.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/companies.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"query": "dental clinics",
"location": "Austin, TX",
"limit": 20
},
timeout=240,
)
print(r.json()){
"status": "success",
"credits_used": 2,
"data": {
"query": "dental clinics",
"location": "Austin, TX",
"search": "dental clinics in Austin, TX",
"results_count": 20,
"with_emails": false,
"emails_found": null,
"next_page_token": "c:eyJxIjoiZGVudGFsIGNsaW5pY3MgaW4gQXVzdGluLCBUWCIs...",
"credits_breakdown": { "pages": 2, "emails": 0, "verified_emails": 0 },
"companies": [
{
"id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"name": "Lakeside Family Dental",
"website": "https://www.lakesidefamilydental.com/",
"phone": "(512) 555-0142",
"address": "1200 Barton Springs Rd, Austin, TX 78704",
"rating": 4.8,
"reviews": 312,
"lat": 30.2616,
"lng": -97.7593,
"categories": ["Dentist", "Cosmetic dentist"],
"hours": ["Monday: 8 AM-5 PM", "Tuesday: 8 AM-5 PM"],
"maps_url": "https://maps.google.com/?cid=1234567890123456789",
"domain": "lakesidefamilydental.com"
}
]
}
}Authentication
Every request carries your key in the api_key query-string parameter. The account is resolved from the key — you don't send a user id.
curl "https://mail.leadgen.tools/v2/api/v1/campaigns.php?api_key=$LEADGEN_API_KEY"
curl -X POST "https://mail.leadgen.tools/v2/api/v1/verify.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"emails": ["jane@example.com"]}'- The key is checked on every call. A missing key returns
401 missing_api_key; an invalid, expired or inactive one401 invalid_api_key. If the license check can't be reached you get503 auth_error— retry shortly. - Every call needs a balance of at least 1 credit, including the free ones (projects, leads, status…). With 0 credits every call answers
402 insufficient_credits. - All data is scoped to the key's account: you only see and change your own projects, leads, sending accounts and campaigns. Anything else answers
404 not_found.
Credits & pricing
Calls spend credits from your LeadGen.tools balance. You are charged for what is delivered: empty results cost 0, and errors cost 0.
| Endpoint | Credits | Details |
|---|---|---|
| Companies | 2 per page | Per maps page of ~20 companies delivered. With with_emails: +1 per company whose email was found on its website, +2 per company whose email came verified from our data partners (grade A / A-). Each page and each email is charged once per account for 30 days. |
| People | 2 / 1 per person | 2 per person delivered with a verified email (A / A-), 1 per person without an email (lookup=false). Nobody found = 0. Each person charged once per account for 30 days. |
| Lead search | 0–10 | 10 when more than 7 results come back; otherwise 1 per result with an email. 0 for no results or a cached answer. |
| Website crawler | 0–15 | 1 per unique valid email on the site's own domain; 1 if only other emails were found; 0 if none or cached. |
| Ad Spy | 0–10 | 10 when more than 7 fresh ads come back; otherwise 1 per fresh ad whose text shows an email. Historical ads and landing-page contact data are free. |
| Places search | 2 per page | Per page of up to 20 places; 0 if the page is empty. |
| Place reviews | 0 or 1 | 1 when reviews are fetched; 0 when they were fetched in the last 7 days. |
| AI email generation | 2–6 | 1 per email written (the first email + each follow-up), minimum 2. |
| Verify, projects, leads, accounts, campaigns, steps, launch, control, status, recipients | 0 | Free (a balance of at least 1 credit is still required to call them). |
| Campaign sending | 1 per prospect | Charged by the sender when a prospect gets its first email; the follow-ups to that prospect are included. |
Never more than your balance
Companies and People count the cost as they go and never spend more than you have. When the balance runs short they return what you can pay for and add a warning to data (for example “Not enough credits: 3 email(s) withheld…”); on Companies each withheld company has email_withheld: true. Top up and repeat the same call: what you already paid for stays free. If even the first page costs more than your balance, you get 402 insufficient_credits.
AI generation checks the full price first and answers 402 if your balance is lower.
credits_used on every response, and data.credits_breakdown on Companies (pages, emails, verified_emails).Errors
Errors use the HTTP status and a JSON body with status: "error", a stable code and a readable message. Branch on code, show message.
| HTTP | code | Meaning |
|---|---|---|
| 400 | missing_param | A required parameter or body field is missing. |
| 400 | invalid_param | A value is not accepted (unknown search engine, bad page_token, too many items, unknown status or category…). |
| 400 | invalid_body | The endpoint needs a JSON body and none was sent. |
| 400 | invalid_json | The body is not valid JSON. |
| 400 | invalid_code | Lead search: the recipe code doesn't exist. |
| 400 | invalid_state | The campaign can't do that in its current status (e.g. launching a campaign that isn't a draft), or it has no steps / no valid sending account. |
| 400 | campaign_error | Creating a campaign or replacing its steps failed (e.g. a step without subject or body_html). Nothing was saved. |
| 400 | launch_error | Launch failed, e.g. “No valid recipients found in project leads.” Nothing changed. |
| 401 | missing_api_key | No api_key parameter. |
| 401 | invalid_api_key | The key is invalid, expired or inactive. |
| 402 | insufficient_credits | Balance below 1 credit, or below the price of the call. |
| 404 | not_found | The project, campaign or sending account doesn't exist in your account. |
| 404 | invalid_user_id | The account behind the key could not be loaded. |
| 405 | method_not_allowed | Wrong HTTP method for that endpoint. |
| 409 | duplicate_project | A project with that name already exists. |
| 500 | db_error | Internal database error. Retry later. |
| 500 | config_error | AI generation is not configured on this server. |
| 502 | provider_error | Companies: the maps data source is unavailable. No credits deducted. |
| 502 | api_error | Places search / reviews: the maps data source returned an error. No credits deducted. |
| 502 | search_error | Lead search / Ad Spy: the search request failed. Retry after a few seconds. |
| 502 | crawl_error | Website crawler: the URL could not be fetched. |
| 502 | ai_error | AI generation failed or returned an unusable answer. No credits deducted — try again. |
| 503 | auth_error | The license check is unreachable. Retry later. |
| 503 | not_available | People search is not available right now, or reply categories are not enabled on this server (Recipients with category). |
Retrying
- 4xx: fix the request; the same call gives the same answer (except
402after a top-up). - 5xx: retry with a growing pause (for example 5 s, 30 s, 2 min). Companies and People are safe to repeat — already-paid items cost 0.
- Network timeout on a slow call: repeat it; see Rate limits for sensible timeouts.
Rate limits & fair use
There is no per-minute request quota on API v1 today. What is enforced is the size of each call:
| Endpoint | Limit per call |
|---|---|
| Companies | limit 1–60 companies; up to 5 maps pages read; up to 30 partner email lookups; query / location ≤ 150 characters |
| People | want 1–3 people; offer ≤ 300, ideal ≤ 200, company ≤ 120 characters |
| Website crawler | Up to 15 pages of the site |
| Ad Spy | Up to 15 landing pages read per call (the rest on later calls) |
| Places search | About 20 places per page, up to 10 pages per search |
| Verify | 20 emails |
| Leads (POST) | 500 leads |
| Leads / Recipients (GET) | limit up to 500 (default 100), page with offset |
| Projects (GET) | 500 most recent |
| Generate | 1–5 follow-ups |
Recommendations
- Call slow endpoints one at a time. Companies with emails (30–120 s for a full call), People (up to about a minute per domain), the crawler (up to 2 min) and Verify (a few seconds per email) do real work on every call.
- Use an HTTP timeout of at least 240 s for Companies and 180 s for People and the crawler.
- Keep other calls under 5 at a time, and don't poll campaign status more than every few minutes — the sender works in the background.
- Reuse results: identical calls are cached (see Caching) and already-paid items are free.
Campaign sending has its own limits: each campaign's daily_limit, each sending account's daily limit, and your plan. See Launch.
Caching
Results are cached so repeated work is fast and, where noted, free.
| Endpoint | Cache | Effect on credits |
|---|---|---|
| Companies | Maps pages 7 days · website contacts 14 days | You pay each page / email once per 30 days; repeats cost 0 |
| People | 30 days (same domain, company, want, offer, ideal, lang, lookup) | Each person once per 30 days |
| Lead search | 7 days (same query, engine, page, results_per_page) | Cached answers cost 0 |
| Website crawler | 14 days (same URL) | Cached answers cost 0 |
| Ad Spy | No response cache: every call searches again. Ads are stored permanently; landing pages are re-read after 7 days | Charged on the fresh results of each call |
| Places search | Always live | — |
| Place reviews | 7 days per place | Cached answers cost 0 (cached: true) |
| AI generation, campaigns, workspace | Never cached | — |
Companies
/api/v1/companies.php/api/v1/companies.phpReal businesses for a niche and a place, from maps data — up to 60 per call, paginated — optionally with the email of each one. Emails are read from each company's own website first; when the site shows none, our data partners are asked for a verified one.
with_emails: +1 per email found on the website, +2 per verified email from our data partners. Each page and email is billed once per account for 30 days.Parameters
querystringrequired*dental clinics. *Not needed when you send page_token. Max 150 characters.locationstringoptionalAustin, TX. The search becomes “query in location”. Max 150 characters.limitintegeroptionaldefault 20with_emailsbooleanoptionaldefault falsefill_missingbooleanoptionaldefault truewith_emails: when the website shows no email, ask our data partners for a verified one (up to 30 per call).page_tokenstringoptionalnext_page_token of a previous response, passed back exactly as received. Continues exactly where it stopped.Response fields
| Field | Description |
|---|---|
search | The search that was run (“query in location”). |
results_count | Companies in this response. |
emails_found | Companies with an email (null without with_emails). |
next_page_token | Pass as page_token for more; null when there are no more. |
credits_breakdown | Credits by kind: pages, emails, verified_emails. |
companies[] | id, name, website, domain, phone, address, rating, reviews, lat, lng, categories, hours, maps_url. |
+ with with_emails | email (the best one), email_grade, email_verified, emails (all found), socials (profile URLs), description (from the site), and email_withheld: true when your balance ran short. |
warning | Only when credits ran short (emails withheld or stopped early). |
Email grades
email_verified is true only for grade A. Run null-grade emails through Verify before sending.
Notes
- The best email prefers the company's own domain and general inboxes (info@, contact@) over no-reply or jobs addresses.
- Social pages and directories listed as a “website” are not read for emails.
- A page is billed as a whole: asking for
limit=5pays the page, and the next call withpage_tokencontinues on that same page for free. - With
with_emailsa full call can take 30–120 s. Use a 240 s timeout.
curl -G "https://mail.leadgen.tools/v2/api/v1/companies.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "query=dental clinics" \
--data-urlencode "location=Austin, TX" \
--data-urlencode "limit=20" \
--data-urlencode "with_emails=true"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
query: "dental clinics",
location: "Austin, TX",
limit: 20,
with_emails: "true"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/companies.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/companies.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"query": "dental clinics",
"location": "Austin, TX",
"limit": 20,
"with_emails": "true"
},
timeout=240,
)
print(r.json()){
"status": "success",
"credits_used": 22,
"data": {
"query": "dental clinics",
"location": "Austin, TX",
"search": "dental clinics in Austin, TX",
"results_count": 20,
"with_emails": true,
"emails_found": 17,
"next_page_token": "c:eyJxIjoiZGVudGFsIGNsaW5pY3MgaW4gQXVzdGluLCBUWCIs...",
"credits_breakdown": { "pages": 2, "emails": 14, "verified_emails": 6 },
"companies": [
{
"id": "ChIJN1t_tDeuEmsRUsoyG83frY4",
"name": "Lakeside Family Dental",
"website": "https://www.lakesidefamilydental.com/",
"phone": "(512) 555-0142",
"address": "1200 Barton Springs Rd, Austin, TX 78704",
"rating": 4.8,
"reviews": 312,
"lat": 30.2616,
"lng": -97.7593,
"categories": ["Dentist", "Cosmetic dentist"],
"hours": ["Monday: 8 AM-5 PM", "Tuesday: 8 AM-5 PM"],
"maps_url": "https://maps.google.com/?cid=1234567890123456789",
"domain": "lakesidefamilydental.com",
"email": "hello@lakesidefamilydental.com",
"email_verified": false,
"email_grade": null,
"emails": ["hello@lakesidefamilydental.com"],
"socials": ["https://www.facebook.com/lakesidefamilydental"],
"description": "Gentle family and cosmetic dentistry in South Austin."
},
{
"id": "ChIJ2eUgeAK1RIYRz2hYd0vBqJc",
"name": "Eastside Smiles",
"website": "https://eastsidesmiles.com/",
"domain": "eastsidesmiles.com",
"email": "maria.lopez@eastsidesmiles.com",
"email_verified": true,
"email_grade": "A",
"emails": ["maria.lopez@eastsidesmiles.com"]
}
]
}
}People (decision makers)
/api/v1/people.php/api/v1/people.phpThe people who decide at a company, ranked by AI for what you sell, with verified emails. Give it the company's domain and your offer; it returns the best contacts first, each with a one-line reason.
lookup=false). Nobody found = 0. Each person is billed once per account for 30 days.Parameters
domainstringrequiredacme.com, https://www.acme.com/about…companystringoptionalwantintegeroptionaldefault 1offerstringoptionalidealstringoptionallangstringoptionaldefault enreason: en or es.lookupbooleanoptionaldefault truetrue: only people with a verified email, looking the email up when needed. false: names and titles (plus an email when one is already known), no email lookups — cheaper, for research.Response fields
| Field | Description |
|---|---|
candidates | How many people were considered before ranking. |
people_count | People returned. |
people[] | rank (1 = best), name, first_name, last_name, title, email, email_verified (grade A), grade (A / A- / null), linkedin, reason. |
message | When nobody was found, what to try next. |
warning | When your balance ran short and people were withheld. |
Notes
- Only A and A- emails are ever returned. If no decision maker has one,
peopleis empty: uselookup=falsefor names, or the company's general email from Companies. - Default to one contact per company (
want=1). When you contact several people at one company, send them separate emails. - Can take up to about a minute per domain when emails have to be looked up. Call it one domain at a time.
503 not_availablemeans decision-maker search is off right now; no credits are deducted.
curl -G "https://mail.leadgen.tools/v2/api/v1/people.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "domain=eastsidesmiles.com" \
--data-urlencode "company=Eastside Smiles" \
--data-urlencode "want=1" \
--data-urlencode "offer=Online booking software for dental clinics"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
domain: "eastsidesmiles.com",
company: "Eastside Smiles",
want: 1,
offer: "Online booking software for dental clinics"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/people.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/people.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"domain": "eastsidesmiles.com",
"company": "Eastside Smiles",
"want": 1,
"offer": "Online booking software for dental clinics"
},
timeout=180,
)
print(r.json()){
"status": "success",
"credits_used": 2,
"data": {
"domain": "eastsidesmiles.com",
"company": "Eastside Smiles",
"lookup": true,
"candidates": 6,
"people_count": 1,
"people": [
{
"rank": 1,
"name": "Maria Lopez",
"first_name": "Maria",
"last_name": "Lopez",
"title": "Owner & Lead Dentist",
"email": "maria.lopez@eastsidesmiles.com",
"email_verified": true,
"grade": "A",
"linkedin": "https://www.linkedin.com/in/maria-lopez-dds",
"reason": "Owner; decides on practice software"
}
]
}
}Lead search
/api/v1/search.phpRuns a search on a web search engine — plain keywords or search operators (“dorks”) — and returns each result with the emails and phone numbers visible in it. The same engine as Lead Spider in the app.
Parameters
querystringrequired*"marketing agency" "new york" "@gmail.com". *Or send code.codestringoptionalquery. Unknown codes answer 400 invalid_code.sestringoptionaldefault googlegoogle, bing, yahoo or duckduckgo.pageintegeroptionaldefault 0results_per_pageintegeroptionaldefault 10Response fields
query, search_engine, page, results_count and results[] with title, link, description, emails[], phones[].
curl -G "https://mail.leadgen.tools/v2/api/v1/search.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "query=marketing agency new york \"@gmail.com\"" \
--data-urlencode "se=google"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
query: "marketing agency new york \"@gmail.com\"",
se: "google"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/search.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/search.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"query": "marketing agency new york \"@gmail.com\"",
"se": "google"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 10,
"data": {
"query": "marketing agency new york \"@gmail.com\"",
"search_engine": "google",
"page": 0,
"results_count": 9,
"results": [
{
"title": "Brightline Marketing — NYC growth agency",
"link": "https://www.brightlinemarketing.example/",
"description": "Brightline Marketing helps NYC restaurants grow... Contact: brightlinenyc@gmail.com (212) 555-0199",
"emails": ["brightlinenyc@gmail.com"],
"phones": ["(212) 555-0199"]
}
]
}
}Website crawler
/api/v1/crawl.phpReads a website — the page you give plus up to 14 of its internal pages — and returns emails, phone numbers, social links and the page's metadata. Placeholder and junk addresses are filtered out.
Parameters
domainstringrequiredhttps://example.com or example.com (https:// is added).Response fields
| Field | Description |
|---|---|
domain | The URL that was read. |
pages_crawled | Pages read (max 15). |
metadata | title, description, keywords, image of the first page. |
emails | Valid emails on the site's own domain. |
all_emails | Every email found, including other domains. |
phones, social_links | Phone numbers and social profile URLs. |
Can take up to 2 minutes. 502 crawl_error when the first page can't be fetched.
curl -G "https://mail.leadgen.tools/v2/api/v1/crawl.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "domain=https://lakesidefamilydental.com"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
domain: "https://lakesidefamilydental.com"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/crawl.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/crawl.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"domain": "https://lakesidefamilydental.com"
},
timeout=180,
)
print(r.json()){
"status": "success",
"credits_used": 2,
"data": {
"domain": "https://lakesidefamilydental.com",
"pages_crawled": 12,
"metadata": {
"title": "Lakeside Family Dental | Austin Dentist",
"description": "Gentle family and cosmetic dentistry in South Austin.",
"keywords": "",
"image": "https://lakesidefamilydental.com/og.jpg"
},
"emails": ["hello@lakesidefamilydental.com", "billing@lakesidefamilydental.com"],
"all_emails": ["hello@lakesidefamilydental.com", "billing@lakesidefamilydental.com"],
"phones": ["(512) 555-0142"],
"social_links": [
"https://www.facebook.com/lakesidefamilydental",
"https://www.instagram.com/lakesidedental"
]
}
}Ad Spy
/api/v1/ads.phpFinds the companies running search ads for a keyword — businesses that already spend on marketing. Every call searches again; every ad ever seen for that keyword is kept, so you also get the history. Each ad's landing page is read for emails, phones and social links.
Parameters
querystringrequiredemergency plumber chicago.sestringoptionaldefault googlegoogle or bing.pageintegeroptionaldefault 0include_historybooleanoptionaldefault truetrue: every ad ever seen for this keyword on any engine. false: only the ads seen on se.Response fields
fresh_count (ads in today's results), historical_count, results_count and results[]:
| Field | Description |
|---|---|
position, title, url, ad_text, search_engine | The ad as last seen. |
first_seen, last_seen | When it was first and last seen for this keyword. |
is_active | true when the ad is in this call's results. |
contact_data | emails, phones, social_links, metadata from the landing page (read up to 15 pages per call, refreshed after 7 days; empty until read). |
The first call for a keyword can take 30–60 s while landing pages are read.
curl -G "https://mail.leadgen.tools/v2/api/v1/ads.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "query=emergency plumber chicago" \
--data-urlencode "se=google"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
query: "emergency plumber chicago",
se: "google"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/ads.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/ads.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"query": "emergency plumber chicago",
"se": "google"
},
timeout=180,
)
print(r.json()){
"status": "success",
"credits_used": 1,
"data": {
"query": "emergency plumber chicago",
"search_engine": "google",
"page": 0,
"fresh_count": 4,
"historical_count": 3,
"results_count": 7,
"results": [
{
"position": 1,
"title": "24/7 Emergency Plumber | Fast Response",
"url": "https://www.rapidflowplumbing.example/",
"ad_text": "Sponsored · Licensed plumbers in 60 minutes. Call now or email help@rapidflowplumbing.example",
"search_engine": "google",
"first_seen": "2026-08-02 14:10:22",
"last_seen": "2026-09-27 09:31:05",
"is_active": true,
"contact_data": {
"emails": ["help@rapidflowplumbing.example"],
"phones": ["(312) 555-0110"],
"social_links": ["https://www.facebook.com/rapidflowplumbing"],
"metadata": { "title": "Rapid Flow Plumbing", "description": "Emergency plumbing in Chicago.", "keywords": "", "image": "" }
}
}
]
}
}Places search
/api/v1/places/search.phpRaw maps listings for a search, one page of about 20 at a time. For prospecting, Companies is usually better: normalized fields, emails in the same call and up to 60 per call.
Parameters
keywordstringrequired*restaurants in Miami. *Not needed with pagetoken.pagetokenstringoptionalnext_page_token of the previous page, passed back unchanged. Up to 10 pages per search.Response fields
keyword, results_count, next_page_token (null on the last page) and results[] with place_id, name, address, phone, website, rating, rating_total, types, weekday_text (opening hours), url (maps link), icon, lat, long. business_status and open_now may be null. When a place's reviews were fetched in the last 7 days they are included as reviews.
Use place_id with Place reviews.
curl -G "https://mail.leadgen.tools/v2/api/v1/places/search.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "keyword=restaurants in Miami"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
keyword: "restaurants in Miami"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/places/search.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/places/search.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"keyword": "restaurants in Miami"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 2,
"data": {
"keyword": "restaurants in Miami",
"results_count": 20,
"next_page_token": "s:eyJxIjoicmVzdGF1cmFudHMgaW4gTWlhbWkiLCJwYWdlIjoyLC...",
"results": [
{
"url": "https://maps.google.com/?cid=9876543210987654321",
"name": "Casa Verde Kitchen",
"address": "450 NW 27th St, Miami, FL 33127",
"phone": "(305) 555-0177",
"website": "https://casaverdekitchen.example/",
"rating": 4.6,
"rating_total": 1284,
"types": ["Restaurant", "Latin American restaurant"],
"weekday_text": ["Monday: 11 AM-10 PM", "Tuesday: 11 AM-10 PM"],
"icon": null,
"open_now": null,
"place_id": "ChIJx8Qm3gC32YgR0bXg4v1a2bc",
"business_status": null,
"lat": 25.8012,
"long": -80.1998,
"cached": false
}
]
}
}Place reviews
/api/v1/places/reviews.phpRecent reviews of a place — useful to personalize an opening line or qualify a business.
cached: true).Parameters
place_idstringrequiredResponse fields
place_id, cached and reviews[] with author_name, author_url, profile_photo_url, rating, text, relative_time_description, time (Unix timestamp).
curl -G "https://mail.leadgen.tools/v2/api/v1/places/reviews.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "place_id=ChIJx8Qm3gC32YgR0bXg4v1a2bc"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
place_id: "ChIJx8Qm3gC32YgR0bXg4v1a2bc"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/places/reviews.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/places/reviews.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"place_id": "ChIJx8Qm3gC32YgR0bXg4v1a2bc"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 1,
"data": {
"place_id": "ChIJx8Qm3gC32YgR0bXg4v1a2bc",
"reviews": [
{
"author_name": "Daniel R.",
"author_url": null,
"profile_photo_url": null,
"rating": 5,
"text": "Best ropa vieja in town, and the staff remembered our names.",
"relative_time_description": "2 weeks ago",
"time": 1757548800
}
],
"cached": false
}
}Verify emails
/api/v1/verify.phpChecks up to 20 emails: format, domain, MX / A records, SPF, DKIM, DMARC, disposable domains and whether the mailbox accepts mail. Optionally stores the result on your leads.
Body (JSON)
emailsstring[]requiredlead_idsinteger[]optionalemails. Each lead's verified becomes 1 (valid) or -1 (invalid).Response fields
verified, valid, invalid counts and results[]: email, valid, verdict (valid · invalid · invalid_format) and details — domain_exists, valid_mx, disposable, has_a, has_spf, has_dkim, has_dmarc, deliverable.
Notes
validfollows the mailbox check (deliverable). Some servers accept every address; others refuse checks — treat results as a strong signal, not a guarantee.- A few seconds per email: send batches of 20 one after another, with a generous timeout.
- Emails graded A by People or Companies are already verified.
curl -X POST "https://mail.leadgen.tools/v2/api/v1/verify.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"emails": [
"hello@lakesidefamilydental.com",
"info@nosuchdomain-xyz.example"
],
"lead_ids": [1841, 1842]
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/verify.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
emails: [
"hello@lakesidefamilydental.com",
"info@nosuchdomain-xyz.example"
],
lead_ids: [1841, 1842]
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/verify.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"emails": [
"hello@lakesidefamilydental.com",
"info@nosuchdomain-xyz.example"
],
"lead_ids": [1841, 1842]
},
timeout=180,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"verified": 2,
"valid": 1,
"invalid": 1,
"results": [
{
"email": "hello@lakesidefamilydental.com",
"valid": true,
"details": {
"domain_exists": true,
"valid_mx": true,
"disposable": false,
"has_a": true,
"has_spf": true,
"has_dkim": false,
"has_dmarc": true,
"deliverable": true
},
"verdict": "valid"
},
{
"email": "info@nosuchdomain-xyz.example",
"valid": false,
"details": {
"domain_exists": false,
"valid_mx": false,
"disposable": false,
"has_a": false,
"has_spf": false,
"has_dkim": false,
"has_dmarc": false,
"deliverable": false
},
"verdict": "invalid"
}
]
}
}Projects
/api/v1/projects.php/api/v1/projects.phpProjects are the prospect lists of your account — the same ones you see under Prospects in the app. A campaign sends to the leads of one project.
GET — query parameters
searchstringoptionalReturns up to the 500 most recent projects: projects_count and projects[] with id, name, type and campaigns[] (id, name, status) already using it.
POST — body (JSON)
namestringrequired409 duplicate_project otherwise).Returns project_id and name.
?search= before creating one.curl -X POST "https://mail.leadgen.tools/v2/api/v1/projects.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Dental clinics Austin — Sept"
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/projects.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Dental clinics Austin — Sept"
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/projects.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"name": "Dental clinics Austin — Sept"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"project_id": 412,
"name": "Dental clinics Austin — Sept"
}
}curl -G "https://mail.leadgen.tools/v2/api/v1/projects.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "search=Dental"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
search: "Dental"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/projects.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/projects.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"search": "Dental"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"projects_count": 1,
"projects": [
{
"id": 412,
"name": "Dental clinics Austin — Sept",
"type": "",
"campaigns": [
{ "id": 88, "name": "Austin dentists — booking", "status": "active" }
]
}
]
}
}Leads
/api/v1/leads.php/api/v1/leads.phpThe prospects inside a project. Add up to 500 per call; duplicates (same email in the same project) are skipped.
GET — query parameters
project_idintegerrequiredlimitintegeroptionaldefault 100offsetintegeroptionaldefault 0Returns total, limit, offset, leads_count and leads[] (newest first): id, email, firstname, lastname, company, website, phones, description, verified (1 valid · -1 invalid · 0 not checked), keyword, social_media.
POST — body (JSON)
project_idintegerrequiredleadsobject[]requiredemail (required), firstname, lastname, company (or title), website (or url), phones (or phone), description, social_media.Returns imported, duplicates, errors and details[] (one line per rejected row).
Notes
- Without a first or last name, one is guessed from the email (
john.doe@→ John Doe). Pass real names when you have them — they go in{firstname}. companyfills the{company}merge field andwebsitethe{website}one; the website is also what the personal first line ({icebreaker}) is written from.- Imported leads have
keyword=api-import.
curl -X POST "https://mail.leadgen.tools/v2/api/v1/leads.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": 412,
"leads": [
{
"email": "maria.lopez@eastsidesmiles.com",
"firstname": "Maria",
"lastname": "Lopez",
"company": "Eastside Smiles",
"website": "https://eastsidesmiles.com"
},
{
"email": "hello@lakesidefamilydental.com",
"company": "Lakeside Family Dental",
"website": "https://lakesidefamilydental.com",
"phone": "(512) 555-0142"
}
]
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/leads.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project_id: 412,
leads: [
{
email: "maria.lopez@eastsidesmiles.com",
firstname: "Maria",
lastname: "Lopez",
company: "Eastside Smiles",
website: "https://eastsidesmiles.com"
},
{
email: "hello@lakesidefamilydental.com",
company: "Lakeside Family Dental",
website: "https://lakesidefamilydental.com",
phone: "(512) 555-0142"
}
]
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/leads.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"project_id": 412,
"leads": [
{
"email": "maria.lopez@eastsidesmiles.com",
"firstname": "Maria",
"lastname": "Lopez",
"company": "Eastside Smiles",
"website": "https://eastsidesmiles.com"
},
{
"email": "hello@lakesidefamilydental.com",
"company": "Lakeside Family Dental",
"website": "https://lakesidefamilydental.com",
"phone": "(512) 555-0142"
}
]
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"imported": 2,
"duplicates": 0,
"errors": 0,
"details": []
}
}Sending accounts
/api/v1/accounts.phpThe mailboxes your campaigns can send from: Gmail / Google Workspace accounts and SMTP accounts connected in the app. Campaigns go out from these accounts, so replies land in your own inbox.
Parameters
typestringoptionalgmail or smtp to list only one kind.Response fields
accounts_count and accounts[]:
- Both kinds:
type,id,email,name,daily_send_limit,sent_today,status. - Gmail
status:connectedorreconnect_required. - SMTP adds
label,smtp_host,smtp_port,smtp_encryption,has_imap(replies can be detected),is_verified;statusisverifiedorunverified.
curl -G "https://mail.leadgen.tools/v2/api/v1/accounts.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/accounts.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/accounts.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"]
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"accounts_count": 2,
"accounts": [
{
"type": "gmail",
"id": 100000017,
"email": "sam@brightbooking.example",
"name": "Sam Carter",
"daily_send_limit": 500,
"sent_today": 12,
"status": "connected"
},
{
"type": "smtp",
"id": 23,
"email": "sam@getbrightbooking.example",
"name": "Sam Carter",
"label": "Outreach domain",
"smtp_host": "smtp.zoho.com",
"smtp_port": 587,
"smtp_encryption": "tls",
"daily_send_limit": 40,
"sent_today": 0,
"has_imap": true,
"is_verified": 1,
"status": "verified"
}
]
}
}Campaigns
/api/v1/campaigns.php/api/v1/campaigns.phpA campaign sends a sequence of emails (a first email and follow-ups) to the leads of one project, from one or more of your sending accounts. POST creates it as a draft; nothing is sent until you launch it.
POST — body (JSON)
namestringrequiredproject_idintegerrequiredsending_accountsobject[]required{"type": "gmail" | "smtp", "id": 123} from Sending accounts. First emails rotate across them.stepsobject[]requiredstep_order (0 = first email, 1+ = follow-ups; defaults to its position), subject, body_html, delay_days (days to wait after the previous email; 0 for the first). See merge fields & spintax.daily_limitintegeroptionaldefault 50batch_sizeintegeroptionaldefault 5Returns campaign_id, name and status: "draft". A step without subject or body_html answers 400 campaign_error and nothing is saved.
GET — list and detail
GET campaigns.php:campaigns_countandcampaigns[](newest first) withid,name,status,project_id,project_name,accounts_display,daily_limit,total_sent,total_replied,total_bounced,total_recipients,created_at,launched_at.GET campaigns.php?id=88:campaignwith the same fields plusbatch_size,updated_at,steps[],recipient_statsandsending_accounts[](type,id,email,label).
Campaign status: draftactivepausedcompletedcancelled
curl -X POST "https://mail.leadgen.tools/v2/api/v1/campaigns.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Austin dentists — booking",
"project_id": 412,
"daily_limit": 40,
"sending_accounts": [
{
"type": "gmail",
"id": 100000017
},
{
"type": "smtp",
"id": 23
}
],
"steps": [
{
"step_order": 0,
"subject": "{Quick question|Idea} for {company}",
"body_html": "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>We help clinics like {company} fill empty chairs with online booking. {Worth a quick look?|Open to a 10-minute call?}</p><p>{sender_name}</p>",
"delay_days": 0
},
{
"step_order": 1,
"subject": "Re: {Quick question|Idea} for {company}",
"body_html": "<p>{Just checking in|Following up} on my note, {firstname|there}. {Any interest?|Should I send a 2-minute video?}</p><p>{sender_name}</p>",
"delay_days": 3
}
]
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/campaigns.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Austin dentists — booking",
project_id: 412,
daily_limit: 40,
sending_accounts: [
{
type: "gmail",
id: 100000017
},
{
type: "smtp",
id: 23
}
],
steps: [
{
step_order: 0,
subject: "{Quick question|Idea} for {company}",
body_html: "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>We help clinics like {company} fill empty chairs with online booking. {Worth a quick look?|Open to a 10-minute call?}</p><p>{sender_name}</p>",
delay_days: 0
},
{
step_order: 1,
subject: "Re: {Quick question|Idea} for {company}",
body_html: "<p>{Just checking in|Following up} on my note, {firstname|there}. {Any interest?|Should I send a 2-minute video?}</p><p>{sender_name}</p>",
delay_days: 3
}
]
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/campaigns.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"name": "Austin dentists — booking",
"project_id": 412,
"daily_limit": 40,
"sending_accounts": [
{
"type": "gmail",
"id": 100000017
},
{
"type": "smtp",
"id": 23
}
],
"steps": [
{
"step_order": 0,
"subject": "{Quick question|Idea} for {company}",
"body_html": "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>We help clinics like {company} fill empty chairs with online booking. {Worth a quick look?|Open to a 10-minute call?}</p><p>{sender_name}</p>",
"delay_days": 0
},
{
"step_order": 1,
"subject": "Re: {Quick question|Idea} for {company}",
"body_html": "<p>{Just checking in|Following up} on my note, {firstname|there}. {Any interest?|Should I send a 2-minute video?}</p><p>{sender_name}</p>",
"delay_days": 3
}
]
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"name": "Austin dentists — booking",
"status": "draft"
}
}Generate emails with AI
/api/v1/campaigns/generate.phpWrites a cold email sequence — the first email plus 1–5 follow-ups — from a short brief. The emails use merge fields, a <p>{icebreaker}</p> line in the first email and plenty of spintax so no two prospects get the same text. It returns the steps; it doesn't save anything. Review them, then pass them to Campaigns or Update steps.
num_followups, minimum 2 (so 2–6). Charged only when the emails come back.Body (JSON)
promptstringrequirednum_followupsintegeroptionaldefault 2custom_system_promptstringoptionalcustom_user_prompt_templatestringoptional{num_followups} and {prompt} as placeholders.Returns steps[] with step_order, subject, body_html, delay_days.
{sender_name}, filled from the sending account when each email goes out. Unsubscribe lines are added automatically — don't ask for them.curl -X POST "https://mail.leadgen.tools/v2/api/v1/campaigns/generate.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "We sell online booking software for dental clinics. Proof: clinics fill 20% more chairs. Goal: a 15-minute demo. Friendly, short, English.",
"num_followups": 2
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/campaigns/generate.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: "We sell online booking software for dental clinics. Proof: clinics fill 20% more chairs. Goal: a 15-minute demo. Friendly, short, English.",
num_followups: 2
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/campaigns/generate.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"prompt": "We sell online booking software for dental clinics. Proof: clinics fill 20% more chairs. Goal: a 15-minute demo. Friendly, short, English.",
"num_followups": 2
},
timeout=120,
)
print(r.json()){
"status": "success",
"credits_used": 3,
"data": {
"steps": [
{
"step_order": 0,
"subject": "{Quick question|A thought|Idea} for {company}",
"body_html": "<p>{Hi|Hey|Hello} {firstname},</p><p>{icebreaker}</p><p>{Clinics like yours|Practices like {company}} {fill|book} about 20% more chairs once patients can book online...</p><p>{Open to|Up for} a {15-minute|quick} demo {this week|next week}?</p><p>{Best|Cheers},<br>{sender_name}</p>",
"delay_days": 0
},
{
"step_order": 1,
"subject": "Re: {Quick question|A thought|Idea} for {company}",
"body_html": "<p>{Just following up|Circling back}, {firstname}...</p>",
"delay_days": 3
},
{
"step_order": 2,
"subject": "Re: {Quick question|A thought|Idea} for {company}",
"body_html": "<p>{Last note from me|One last try}...</p>",
"delay_days": 4
}
]
}
}Update steps
/api/v1/campaigns/steps.phpReplaces all the emails of a campaign with the ones you send. Only for draft or paused campaigns (pause it first with control). Emails already sent stay as they were; future sends use the new text.
Body (JSON)
campaign_idintegerrequiredstepsobject[]requiredReturns campaign_id, steps_count, message. Other statuses answer 400 invalid_state.
curl -X PUT "https://mail.leadgen.tools/v2/api/v1/campaigns/steps.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": 88,
"steps": [
{
"step_order": 0,
"subject": "{Quick question|Idea} for {company}",
"body_html": "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>...</p>",
"delay_days": 0
},
{
"step_order": 1,
"subject": "Re: {Quick question|Idea} for {company}",
"body_html": "<p>{Just checking in|Following up}...</p>",
"delay_days": 4
}
]
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/campaigns/steps.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
campaign_id: 88,
steps: [
{
step_order: 0,
subject: "{Quick question|Idea} for {company}",
body_html: "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>...</p>",
delay_days: 0
},
{
step_order: 1,
subject: "Re: {Quick question|Idea} for {company}",
body_html: "<p>{Just checking in|Following up}...</p>",
delay_days: 4
}
]
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.put(
"https://mail.leadgen.tools/v2/api/v1/campaigns/steps.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"campaign_id": 88,
"steps": [
{
"step_order": 0,
"subject": "{Quick question|Idea} for {company}",
"body_html": "<p>{Hi|Hello} {firstname|there},</p><p>{icebreaker}</p><p>...</p>",
"delay_days": 0
},
{
"step_order": 1,
"subject": "Re: {Quick question|Idea} for {company}",
"body_html": "<p>{Just checking in|Following up}...</p>",
"delay_days": 4
}
]
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"steps_count": 2,
"message": "Steps updated successfully."
}
}Launch
/api/v1/campaigns/launch.phpStarts a draft campaign: loads every lead of its project that has an email as a recipient and sets the campaign active. From then on LeadGen.tools sends in the background.
Body (JSON)
campaign_idintegerrequireddraft status.Returns campaign_id, status: "active" and recipients_loaded.
Before launching
- The campaign needs at least one step and one valid sending account (
400 invalid_stateotherwise). - People who unsubscribed or said they weren't interested in an earlier campaign of yours are never loaded. No recipient left →
400 launch_error. - For a lead with several emails, the first one is used.
How sending works
- Emails go out in business hours on weekdays, a few at a time with human-like pauses, never over the campaign's
daily_limitor each account's daily limit. - First emails rotate across the campaign's sending accounts; each prospect's follow-ups come from the account that sent its first email, as replies in the same conversation.
- Follow-ups are sent only to people who haven't replied. Replies are checked automatically and classified by AI (see Recipients).
- Every email carries a short opt-out line and an unsubscribe header; people who opt out are added to your suppression list.
- If the bounce rate gets too high the campaign is paused to protect your domain.
- Out of credits, the campaign isn't paused: follow-ups keep going and new prospects wait until you top up.
curl -X POST "https://mail.leadgen.tools/v2/api/v1/campaigns/launch.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": 88
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/campaigns/launch.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
campaign_id: 88
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/campaigns/launch.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"campaign_id": 88
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"status": "active",
"recipients_loaded": 37
}
}Pause, resume or cancel
/api/v1/campaigns/control.phpChanges a running campaign's status.
Body (JSON)
campaign_idintegerrequiredactionstringrequiredpause (active → paused), resume (paused → active) or cancel (active or paused → cancelled, final).Returns campaign_id, previous_status, status, message. Any other transition answers 400 invalid_state.
curl -X POST "https://mail.leadgen.tools/v2/api/v1/campaigns/control.php?api_key=$LEADGEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"campaign_id": 88,
"action": "pause"
}'const res = await fetch(
`https://mail.leadgen.tools/v2/api/v1/campaigns/control.php?api_key=${process.env.LEADGEN_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
campaign_id: 88,
action: "pause"
}),
}
);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.post(
"https://mail.leadgen.tools/v2/api/v1/campaigns/control.php",
params={"api_key": os.environ["LEADGEN_API_KEY"]},
json={
"campaign_id": 88,
"action": "pause"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"previous_status": "active",
"status": "paused",
"message": "Campaign paused."
}
}Status
/api/v1/campaigns/status.phpA light summary of a campaign's progress, for polling.
Parameters
campaign_idintegerrequiredReturns campaign_id, name, status, daily_limit, total_sent, total_replied, total_bounced, sent_today, steps_count, launched_at, updated_at and recipient_stats:
pending = waiting for the first email · in_progress = in the sequence · completed = every email sent, no reply.
curl -G "https://mail.leadgen.tools/v2/api/v1/campaigns/status.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "campaign_id=88"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
campaign_id: 88
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/campaigns/status.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/campaigns/status.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"campaign_id": 88
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"name": "Austin dentists — booking",
"status": "active",
"daily_limit": 40,
"total_sent": 61,
"total_replied": 4,
"total_bounced": 1,
"sent_today": 9,
"steps_count": 3,
"recipient_stats": {
"total": 37,
"pending": 6,
"in_progress": 24,
"completed": 2,
"replied": 4,
"bounced": 1,
"unsubscribed": 0
},
"launched_at": "2026-09-21 10:02:11",
"updated_at": "2026-09-27 11:45:40"
}
}Recipients & replies
/api/v1/campaigns/recipients.phpEveryone in a campaign with where they are in the sequence — and, once they answer, what they said: every reply is classified by AI with a one-line summary.
Parameters
campaign_idintegerrequiredstatusstringoptionalpending, in_progress, completed, replied, bounced, unsubscribed.categorystringoptionalhot (= interested + meeting + question) or a comma list of interested, meeting, question, not_interested, unsubscribe, out_of_office, wrong_person, other. Combines with status.limitintegeroptionaldefault 100offsetintegeroptionaldefault 0Response fields
total (matching the filters), limit, offset, status_filter, category_filter, recipients_count and recipients[]:
| Field | Description |
|---|---|
id, email, firstname, lastname | The prospect. |
status, current_step | Where they are in the sequence. |
last_sent_at, replied_at | Last email sent, and when they replied. |
sent_via_type, sent_via_account_id | The sending account used (gmail / smtp + id). |
reply_category | What the reply is (list below). null until they answer. |
reply_summary | One line: what they said. |
reply_text | The text of the reply. |
Reply categories
- A reply stops that prospect's sequence (status
replied), except out of office: the next email simply waits until they are back. unsubscribe→ statusunsubscribed;unsubscribeandnot_interestedare added to your suppression list, so no future campaign contacts them.- Answer the hot ones from your own inbox — the replies arrived there.
categoryon a server without reply intelligence answers503 not_available.
curl -G "https://mail.leadgen.tools/v2/api/v1/campaigns/recipients.php" \
--data-urlencode "api_key=$LEADGEN_API_KEY" \
--data-urlencode "campaign_id=88" \
--data-urlencode "category=hot"const params = new URLSearchParams({
api_key: process.env.LEADGEN_API_KEY,
campaign_id: 88,
category: "hot"
});
const res = await fetch(`https://mail.leadgen.tools/v2/api/v1/campaigns/recipients.php?${params}`);
const { status, credits_used, data } = await res.json();
console.log(status, credits_used, data);import os
import requests
r = requests.get(
"https://mail.leadgen.tools/v2/api/v1/campaigns/recipients.php",
params={
"api_key": os.environ["LEADGEN_API_KEY"],
"campaign_id": 88,
"category": "hot"
},
timeout=60,
)
print(r.json()){
"status": "success",
"credits_used": 0,
"data": {
"campaign_id": 88,
"total": 2,
"limit": 100,
"offset": 0,
"status_filter": null,
"category_filter": "hot",
"recipients_count": 2,
"recipients": [
{
"id": 5120,
"email": "maria.lopez@eastsidesmiles.com",
"firstname": "Maria",
"lastname": "Lopez",
"status": "replied",
"current_step": 1,
"last_sent_at": "2026-09-24 10:17:03",
"replied_at": "2026-09-24 13:40:55",
"sent_via_type": "gmail",
"sent_via_account_id": 100000017,
"reply_category": "meeting",
"reply_summary": "Wants a demo next Tuesday afternoon",
"reply_text": "Hi Sam, sounds interesting. Could we do Tuesday after 2pm? Maria"
},
{
"id": 5127,
"email": "office@hillcountrydental.example",
"firstname": "Office",
"lastname": "",
"status": "replied",
"current_step": 2,
"last_sent_at": "2026-09-26 09:05:40",
"replied_at": "2026-09-26 15:22:10",
"sent_via_type": "smtp",
"sent_via_account_id": 23,
"reply_category": "question",
"reply_summary": "Asks if it works with their scheduling system",
"reply_text": "Does this connect with our current scheduling software?"
}
]
}
}Build a full outbound pipeline
From “dental clinics in Austin” to replies in your inbox, entirely through the API.
- Find companies
GET companies.php?query=dental clinics&location=Austin, TX&limit=60&with_emails=true. Repeat withpage_tokenuntil you have enough. Budget: 2 per page + 1–2 per email. - Find the decision maker (optional) — for each company with a
domain,GET people.php?domain=…&offer=…&want=1, one at a time. Keep the company email when nobody is found. - Clean — send the emails with
email_grade: nulltoPOST verify.phpin batches of 20 and drop the invalid ones. - Store —
POST projects.php(or reuse one found with?search=), thenPOST leads.phpwithfirstname,lastname,company,websitefor each prospect. - Write —
POST campaigns/generate.phpwith a clear brief. Read and edit the emails before using them. - Create —
GET accounts.phpto pick the sending accounts, thenPOST campaigns.phpwith the project, accounts and steps. It's a draft. - Launch —
POST campaigns/launch.php. Sending costs 1 credit per prospect contacted, follow-ups included. - Follow — poll
GET campaigns/status.phpevery few hours, andGET campaigns/recipients.php?category=hotfor the people who want to talk.
import os, time, requests
BASE = "https://mail.leadgen.tools/v2/api/v1/"
KEY = {"api_key": os.environ["LEADGEN_API_KEY"]}
def call(method, path, params=None, body=None, timeout=240):
r = requests.request(method, BASE + path, params={**KEY, **(params or {})}, json=body, timeout=timeout)
j = r.json()
if j["status"] != "success":
raise RuntimeError(j["code"] + ": " + j["message"])
print(path, "credits_used =", j["credits_used"])
return j["data"]
# 1. Companies with emails
found = call("GET", "companies.php", {"query": "dental clinics", "location": "Austin, TX",
"limit": 40, "with_emails": "true"})
leads = []
for c in found["companies"]:
if not c.get("email"):
continue
lead = {"email": c["email"], "company": c["name"], "website": c["website"] or ""}
# 2. Decision maker (optional, one domain at a time)
if c.get("domain"):
p = call("GET", "people.php", {"domain": c["domain"], "company": c["name"], "want": 1,
"offer": "Online booking software for dental clinics"}, timeout=180)
if p["people"]:
best = p["people"][0]
lead.update(email=best["email"], firstname=best["first_name"], lastname=best["last_name"])
leads.append(lead)
# 3. Verify website emails that are not graded (skipped here for brevity: POST verify.php, 20 at a time)
# 4. Project + leads
project = call("POST", "projects.php", body={"name": "Austin dentists " + time.strftime("%Y-%m-%d")})
call("POST", "leads.php", body={"project_id": project["project_id"], "leads": leads})
# 5. Emails
steps = call("POST", "campaigns/generate.php", body={
"prompt": "Online booking software for dental clinics. Goal: a 15-minute demo. Friendly, short.",
"num_followups": 2})["steps"]
# 6. Campaign from every connected account
accounts = [{"type": a["type"], "id": a["id"]} for a in call("GET", "accounts.php")["accounts"]]
camp = call("POST", "campaigns.php", body={"name": "Austin dentists", "project_id": project["project_id"],
"sending_accounts": accounts, "steps": steps, "daily_limit": 40})
# 7. Launch (review first!) and 8. follow the replies later
call("POST", "campaigns/launch.php", body={"campaign_id": camp["campaign_id"]})
hot = call("GET", "campaigns/recipients.php", {"campaign_id": camp["campaign_id"], "category": "hot"})
for r in hot["recipients"]:
print(r["email"], r["reply_category"], "-", r["reply_summary"])Send with your own tool
Already sending with another platform, your own SMTP or a CRM with sequences? Use LeadGen.tools only to find and clean the prospects, and export them.
- Find —
companies.phpwithwith_emails=true, andpeople.phpwhen you want a named decision maker. - Clean — verify
null-grade emails withverify.php; drop invalid ones and duplicates. - Export — write a CSV or push the rows to your tool's API. Useful columns:
company, website, domain, first_name, last_name, title, email, email_grade, phone, address, rating, reviews.
import csv, os, requests
BASE = "https://mail.leadgen.tools/v2/api/v1/"
KEY = os.environ["LEADGEN_API_KEY"]
data = requests.get(BASE + "companies.php", params={
"api_key": KEY, "query": "roofing contractors", "location": "Denver, CO",
"limit": 60, "with_emails": "true"}, timeout=240).json()["data"]
cols = ["company", "website", "domain", "email", "email_grade", "phone", "address", "rating", "reviews"]
with open("prospects.csv", "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=cols)
w.writeheader()
for c in data["companies"]:
if c.get("email"):
w.writerow({"company": c["name"], "website": c["website"], "domain": c["domain"],
"email": c["email"], "email_grade": c["email_grade"] or "", "phone": c["phone"],
"address": c["address"], "rating": c["rating"], "reviews": c["reviews"]})Merge fields & spintax
Subjects and bodies of campaign steps are personalized for every prospect when each email is sent.
| Field | Filled with |
|---|---|
{firstname}, {lastname} | The lead's first / last name. |
{email} | The lead's email. |
{company} | The lead's company (company in Leads). |
{website} | The lead's website. |
{icebreaker} | A personal first line about that prospect, written by AI from its website when the first email is sent. Put it alone in a paragraph: <p>{icebreaker}</p>. When there is nothing specific to say, the whole paragraph is removed. |
{sender_name}, {sender_email}, {sender_company} | The signer and the sending account. An empty company line under the name is removed. |
Fallbacks
Write {field|fallback} to use a word when the field is empty: {Hi|Hello} {firstname|team}, becomes “Hi Maria,” or “Hello team,”.
Spintax
{Hi|Hey|Hello} picks one option at random for each email, so no two prospects receive exactly the same text — good for deliverability. Merge fields are filled first; any {a|b|c} left is spintax. Don't nest braces.
<p>{Hi|Hey|Hello} {firstname|there},</p>
<p>{icebreaker}</p>
<p>{We help|We work with} clinics like {company} {fill more chairs|get more bookings} with online booking.</p>
<p>{Worth a quick look?|Open to a 15-minute demo?}</p>
<p>{sender_name}<br>{sender_company}</p>Added automatically
- A short opt-out line (“just reply no”) and an unsubscribe header on every email — don't write your own unsubscribe link.
- Follow-ups are sent as replies in the same conversation as the first email.
AI agents & OpenClaw
The LeadGen.tools skill teaches an AI agent the whole API: which endpoint to use, what things cost, how to page, when to ask before spending, and how to run a campaign end to end. Your agent can then do things like:
Find 20 dental clinics in Austin with their owners' emails and draft a 3-email sequenceInstall in OpenClaw
- Install the skillFrom ClawHub:
Or straight from GitHub:ClawHub
openclaw skills install @leadgen/leadgen-toolsOr by hand: copy theGitHubopenclaw skills install git:leadgentools/leadgen-tools-skill@mainleadgen-toolsfolder to~/.openclaw/skills/leadgen-tools(or<workspace>/skills/leadgen-tools). - Give it your API keyIn
~/.openclaw/openclaw.json:or as an environment variable:~/.openclaw/openclaw.json{ "skills": { "entries": { "leadgen-tools": { "apiKey": "YOUR_API_KEY" } } } }export LEADGEN_API_KEY="…". Where to find your key. - AskStart a new session and describe what you want in plain words — who to target, where, how many, what you sell, and whether to send from LeadGen.tools or export.
Other agents
The skill is a standard SKILL.md folder, so it also works with other agents that read skills. For Claude Code, copy it to ~/.claude/skills/leadgen-tools and set LEADGEN_API_KEY in the environment. Any agent that can make HTTP requests can also use this reference directly.
Changelog
v1.5 September 2026
- Recipients: new
categoryfilter (hot= interested + meeting + question, or a list of categories), and each recipient now hasreply_category,reply_summaryandreply_text. - API & Agents page in the app: see and copy your API key, and connect an AI agent in three steps.
- New documentation.
v1.4 September 2026
- Companies: real businesses for a niche + city in one call (up to 60, paginated), optionally with the email of each one — read from its website, or verified by our data partners.
- People: decision makers at a company domain, ranked by AI for what you sell, with verified emails.
- Items you already paid for are free to fetch again for 30 days.
- Agent Skill: a ready-made skill lets AI agents (Claude, OpenClaw…) use the whole API.
v1.3 February 2026
- Campaign Management API (OpenClaw): full campaign lifecycle via API — create projects, import leads, verify emails, generate AI email sequences, create & launch campaigns, and monitor progress.
- New endpoints: Projects, Leads, Email Verification, Sending Accounts, Campaigns, AI Email Generation, Update Steps, Launch, Control, Status, Recipients.
- AI Email Generation: spintax-powered email sequences written by AI. 1 credit per email generated.
- Email Verification: domain, MX, SPF, DKIM, DMARC, disposable check, and mailbox deliverability validation.
v1.2 February 2026
- Ad Spy — historical ads: ads are stored permanently. Every search adds new ads to the history. Use
first_seen,last_seenandis_activeto track how long ads run. - Ad Spy — landing-page contact data: each ad's landing page is read for
emails,phones,social_linksand pagemetadata, returned incontact_data. - Longer caching: search cache from 1 hour to 7 days; crawl cache from 24 hours to 14 days.
- New parameter:
include_historyon Ad Spy.
v1.1 January 2026
- Places search: maps business listings.
- Place reviews: reviews of a place, cached 7 days.
v1.0 December 2025
- Initial release with Lead Search, Website Crawler and Ad Spy.
- License key authentication.
- Credit-based billing with automatic caching.
LeadGen.tools API v1 · Your API key · Back to top ↑