Leadmaster for developers and AI assistants
Ask for leads from Claude, ChatGPT or Cursor, or call the API from your own code.
How it works
Two steps, and the first one costs nothing while you have free leads. First you estimate a list: tell us who you are looking for, where, and which contact a lead must have. We answer with how many businesses we found, a few examples, and the most the list can cost. No list starts and no lead is charged.
Then you confirm the estimate. Confirming starts the list and holds its cost from your balance. You are charged only for ready leads, the ones that pass your contact rule, and never more than the estimate you approved. When the list is done you read the leads or download a CSV.
Every key and assistant on an account can read all its lists, read their leads and make
download links. Only the key or assistant that made an estimate can start it, and only the one
that started a list can unlock its extras or continue it, so each key's monthly spending limit
holds. Trying it with another key answers not_found. Money in the API is in microdollars, where
1 USD = 1,000,000. So 800000 is $0.80.
estimate -> confirm (holds credit) -> read leads or CSV
Connect Claude or ChatGPT
You need a Leadmaster account. No key and no code.
- In Claude or ChatGPT, add a custom connector.
- Paste the Leadmaster address:
https://leadmaster.tech/mcp - Click Connect.
- Sign in to Leadmaster if asked, then click Allow.
Then ask in plain words, for example "Find 200 dentists in Austin with a phone number and tell me what it would cost." The assistant shows you the estimate and asks before it starts anything. You can set a monthly spending limit for each connection, and disconnect it any time, on the AI assistants page of your dashboard.
The assistant has eleven tools:
get_balance: your balance and free leads.find_place: turns a place name into a region, a city name alone too.search_categories: finds business categories by name.suggest_categories: suggests who is likely to buy what you sell.estimate_list: an estimate, by business types, a name keyword, or both (see the search fee).max_leadsis required: 10 to 10,000, or"all". When a new search costs a fee, it first answersneedsFeeOkwith the fee and searches nothing; the assistant asks you, and only after you agree calls it again withaccept_search_fee: true.start_list: confirms an estimate. It only runs after you say yes.unlock_extras: shows the price of extra leads a finished list already checked, and unlocks them only after you say yes.continue_list: an estimate (no search fee) of a follow-up list for the rest of what a list found, at the normal price.get_list: progress, cost and a 24 hour CSV link.get_leads: a page of leads, 25 by default, at most 100.list_lists: your newest 50 lists.
Use a key
For scripts and servers, create a key in your dashboard under Developers, then API keys. You see the key once, so copy it. Keep it secret. You can give each key a monthly spending limit and revoke it any time.
Send the key as a bearer token. This checks your balance:
export LEADMASTER_KEY=lm_live_your_key_here
curl https://leadmaster.tech/v1/balance \
-H "Authorization: Bearer $LEADMASTER_KEY"
The answer:
{
"micros": 12500000,
"freeLeads": 25,
"heldMicros": 0,
"heldFreeLeads": 0,
"history": []
}
The full description of the API is in openapi.json (OpenAPI 3.1).
Cursor and Claude Code
These tools connect with a key instead of the Connect button. Put the key in the
LEADMASTER_KEY environment variable and point the tool at the MCP address.
Cursor, in .cursor/mcp.json:
{
"mcpServers": {
"leadmaster": {
"url": "https://leadmaster.tech/mcp",
"headers": { "Authorization": "Bearer ${env:LEADMASTER_KEY}" }
}
}
}
Claude Code, in .mcp.json:
{
"mcpServers": {
"leadmaster": {
"type": "http",
"url": "https://leadmaster.tech/mcp",
"headers": { "Authorization": "Bearer ${LEADMASTER_KEY}" }
}
}
}
Endpoints
The base address is https://leadmaster.tech/v1. Every route needs the
Authorization: Bearer header, except the signed CSV link. Bodies and answers are
JSON. Examples below use one list, 7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13.
GET /v1/balance
Your balance, what running lists are holding, and your newest balance changes: the 20 newest
by default. Query: historyLimit, 0 to 200, for more or fewer. See the answer above.
GET /v1/places
Turns a place into a region. Query: q, 1 to 80 characters: a country name, a
city name, or "City, Country". A city name alone (q=Bogor) gives the cities with that
name in the country the request comes from and the most searched ones (the US, the UK, Canada, Australia, India,
Indonesia, Germany and France), the largest first. For an AI connector, the request usually comes from the
connector's own servers, not from your country. Add the country for anywhere else.
An unknown place gives an empty places list and a hint that says what to
try: the country too, for example "Bogor, Indonesia".
curl -G https://leadmaster.tech/v1/places \
-H "Authorization: Bearer $LEADMASTER_KEY" \
-d "q=Austin, United States"
{
"places": [
{
"label": "Austin, Texas, United States",
"countryCode": "US",
"locationCoordinate": "30.2672,-97.7431,25",
"kind": "city"
}
]
}
GET /v1/categories
Business categories whose name contains q (up to 60 characters). Estimates accept only these names.
curl -G https://leadmaster.tech/v1/categories \
-H "Authorization: Bearer $LEADMASTER_KEY" \
-d "q=dent"
{ "categories": ["dentist", "dental_clinic", "dental_lab"] }
POST /v1/categories/suggest
Describe what you sell in text, 1 to 500 characters, and get the categories most
likely to buy it. It waits at most 15 seconds; when suggestions are not available it answers
503 suggest_unavailable, and you can search categories by name instead.
curl https://leadmaster.tech/v1/categories/suggest \
-H "Authorization: Bearer $LEADMASTER_KEY" \
-H "Content-Type: application/json" \
-d '{ "text": "Appointment reminder software for small dental practices" }'
{
"categories": ["dentist", "dental_clinic"],
"idealCustomer": "Small independent dental practices that book by phone.",
"placeHint": null
}
POST /v1/estimates
An estimate. rule is the contact a lead must have to count and be charged:
any, phone, social, email, instagram,
tiktok or raw. social is any Instagram, TikTok, Facebook or LinkedIn profile;
instagram and tiktok count only that platform's profile, at the social price.
raw is the raw list: every business found, with its phone and website exactly as Google Maps
lists them (not checked, no phone type or WhatsApp link), at $3 per 1,000 ($2 per 1,000 over 100,000 ready
leads in 30 days). A raw list starts at 50 leads: maxLeads under 50, or a search that
finds fewer, is a 400 invalid_max_leads.
maxLeads is required: 10 to 10,000, or "all" for everything we find.
Optional: idealCustomer (ranks by fit), description,
onePerBrand (true by default) and acceptedFeeMicros.
Nothing is guessed. A field we do not know is refused with 400 invalid_body, naming
it (unknown field max_leads; did you mean maxLeads?), and a category that is not in
the catalog is refused with 400 invalid_categories, naming it and the nearest one
(unknown category: dental_laboratory; did you mean dental_lab?). Nothing is
searched or charged.
keyword also finds businesses whose name contains it, 2 to 60 letters, digits, spaces
or & ' - ., for example "photobooth" when no category fits exactly.
Send categories, a keyword, or both. Each business counts once, and
totalFound adds the two counts, so it can be a little high.
The search fee. An account that never bought credit estimates for free while it has
free leads. An account that bought or was given credit pays $0.05 for each new search
(search_fee in the balance history); it comes back when a list from that search starts
(search_fee_credit), so starting a list costs nothing extra. searchFeeMicros
in the answer is what starting it gives back, and balanceMicros is after the fee. The same
search again within 24 hours (any size, any rule) reuses the count: no fee. With no free
leads and less balance than one search, the estimate is refused with 402 search_needs_credit.
A key's or connection's monthly limit counts its search fees until they come back; a search that would
pass it is refused with 402 spend_limit, before anything is searched. Optional
acceptedFeeMicros: the most search fee the person agreed to; a search that would cost more is
refused with 409 fee_changed (it says the real fee) and costs nothing. Left out, there is no such limit.
curl https://leadmaster.tech/v1/estimates \
-H "Authorization: Bearer $LEADMASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"categories": ["dentist"],
"countryCode": "US",
"locationCoordinate": "30.2672,-97.7431,25",
"placeLabel": "Austin, United States",
"rule": "phone",
"maxLeads": 200
}'
{
"jobId": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
"totalFound": 412,
"sample": [
{ "title": "Maple & Main Dental Studio", "category": "dentist", "city": "Austin", "rating": 4.8 }
],
"maxLeads": 200,
"priceMicros": 4000,
"maxCostMicros": 800000,
"freeLeadsApplied": 0,
"balanceMicros": 12500000
}
priceMicros is per ready lead and maxCostMicros is the most this
list can cost. The jobId is the estimate id, and becomes the list id when you
confirm it.
POST /v1/estimates/{id}/confirm
Starts the list and holds its cost. Only an estimate made by the same key can be confirmed. No body.
curl -X POST https://leadmaster.tech/v1/estimates/7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13/confirm \
-H "Authorization: Bearer $LEADMASTER_KEY"
{ "ok": true }
GET /v1/lists
Your newest 50 lists. An estimate nobody confirmed is not a list yet.
{
"lists": [
{
"id": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
"status": "done",
"label": "Dentist in Austin, United States",
"categories": ["dentist"],
"rule": "phone",
"maxLeads": 200,
"readyLeads": 187,
"createdAt": 1792500000000,
"finishedAt": 1792500420000,
"resultsDeleted": false
}
]
}
GET /v1/lists/{id}
Progress while the list runs (status is running,
finalizing or done). Once done it shows what it found, what it
cost, and when the results are deleted.
{
"id": "7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13",
"status": "done",
"label": "Dentist in Austin, United States",
"rule": "phone",
"maxLeads": 200,
"readyLeads": 187,
"stopReason": "completed",
"totalFound": 412,
"pulled": 412,
"counts": { "ready": 187, "not_ready": 61 },
"maxCostMicros": 800000,
"charged": { "micros": 748000, "freeLeads": 0 },
"keepUntil": 1795092420000,
"parentJobId": null,
"followUpJobId": null,
"unlock": null,
"rest": null
}
Once done: unlock is the quote for extra ready leads it already checked (the same
as GET /v1/lists/{id}/unlock), or null when there are none.
rest is { "remaining": 225 } when it stopped at its cap with businesses
left to check (see continue below), or null. parentJobId is the list
this one continues, and followUpJobId the follow-up list that continues this one,
each null when there is none.
GET /v1/lists/{id}/leads
A page of leads from a finished list. Query: view is ready (the
default, with contacts) or other (the businesses that did not meet the rule,
without contacts); limit is 1 to 200, default 50; offset starts at
0.
curl "https://leadmaster.tech/v1/lists/7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13/leads?limit=1" \
-H "Authorization: Bearer $LEADMASTER_KEY"
{
"leads": [
{
"name": "Maple & Main Dental Studio",
"category": "dentist",
"address": "412 Maple Street",
"city": "Austin",
"country": "US",
"phone": "+15125550123",
"phone_type": "mobile",
"whatsapp_url": "https://wa.me/15125550123",
"email": "hello@mapleandmain.example.com",
"email_status": "valid",
"website": "https://mapleandmain.example.com",
"instagram": "https://instagram.com/mapleandmaindental.example",
"rating": "4.8",
"reviews": "127",
"fit": "0.92",
"status": "ready"
}
],
"total": 187
}
Every lead value is text, even ratings and counts. A lead also carries tiktok, facebook, linkedin, chain and google_maps_url, left out of this example to keep it short. A profile we did not find is an empty string.
POST /v1/lists/{id}/download-link
A fresh link to the finished list as a CSV. The link works for 24 hours and needs no key, so treat it like a password.
{
"url": "https://leadmaster.tech/v1/files/eyJ...signed",
"expiresAt": 1792586820000
}
GET /v1/lists/{id}/unlock
A list that stopped at its cap often has more ready leads already checked. This is what
unlocking them costs now: at most as many as the list's own size (the first ones it checked),
your free leads first, one per lead, then the rest at the normal price (for a follow-up, the
first list's). extras 0 means there is nothing to unlock, or the
list was unlocked already: a list unlocks once, and extras past that cap stay locked and are
never charged. The same numbers show on GET /v1/lists/{id} as unlock.
{ "extras": 6, "freeLeadsApplied": 0, "micros": 30000, "priceMicros": 5000 }
POST /v1/lists/{id}/unlock
Unlocks the quoted extras and charges the quote. Send back the micros you
showed: if the price changed since (free leads used elsewhere), the answer is
quote_changed and nothing is charged. A second call charges nothing and answers
the same. The leads and the CSV include the unlocked leads right away. Only lists made by the
same key can be unlocked.
curl -X POST https://leadmaster.tech/v1/lists/7c1f0b9e-3d52-4b7a-9a41-0f6c2e8d5a13/unlock \
-H "Authorization: Bearer $LEADMASTER_KEY" -H "Content-Type: application/json" \
-d '{ "micros": 30000 }'
{ "ok": true, "unlocked": 6, "micros": 30000, "freeLeads": 0 }
POST /v1/lists/{id}/continue
An estimate for a follow-up list that checks the rest of what a list found (it stopped at its
cap, and rest on the list says about how many are left). Every lead is at the
normal price: the lower of the first list's price and your price today. Free leads go first.
You are charged for the ready leads it finds, never more than maxCostMicros.
Body: maxLeads, 10 to 10,000, or "all" (or leave it out) for all that
are left. Any other field is refused with invalid_body.
Businesses already in the list are never included or charged again. Start it with
POST /v1/estimates/{id}/confirm, like any estimate. A list continues once:
after one follow-up started, the answer is already_continued. Below: a follow-up of
200 phone leads at $0.005 each.
{
"jobId": "2b8e6d1a-0c4f-4f7e-8d3a-5e9b1c7a2f60",
"totalFound": 225,
"sample": [],
"maxLeads": 200,
"priceMicros": 5000,
"maxCostMicros": 1000000,
"freeLeadsApplied": 0,
"balanceMicros": 5000000
}
GET /v1/files/{token}
Downloads the CSV for a link from the route above. It is the one route that takes no key. An old link answers link_expired, a made-up one invalid_link.
Errors
Every error has the same shape. Branch on code, show message to
people.
{ "error": { "code": "rate_limited", "message": "Too many requests: at most 10 estimates a minute per account. Try again in a minute." } }
An assistant's tools answer the same codes, named after the tool's own arguments: a bad
must_have is invalid_must_have, a bad place is
invalid_place, and the message says max_leads, not maxLeads.
| Code | HTTP | What it means | What to do |
|---|---|---|---|
unauthorized | 401 | The key is missing, wrong or revoked. | Send Authorization: Bearer with a live key. |
account_blocked | 403 | This account is blocked. | Contact support. |
account_review | 403 | This account is under review and cannot start or estimate lists yet. | Wait for the review, or contact support. |
invalid_<field> | 400 | A field is missing or wrong. The code names it, for example invalid_categories or invalid_max_leads. | Fix that field and send it again. |
invalid_body | 400 | The body is not a JSON object, or has a field we do not know. The message names it, with the field you probably meant. | Rename or remove that field. |
not_found | 404 | No such list or estimate on this account. Starting, unlocking or continuing also answers this with a key or assistant other than the one that made it. | Use an id from this account; spend with the key that made it. |
search_needs_credit | 402 | An estimate was refused before any search: the account has no free leads left and less balance than one search costs. | Top up on the Balance page, then estimate again. |
fee_changed | 409 | The search would cost more than acceptedFeeMicros. | Ask the person, then estimate again with the fee the message names. |
insufficient_balance | 402 | Your balance does not cover the most this list can cost. | Top up on the Balance page, then confirm again. |
spend_limit | 402 | The monthly spending limit of this key or connection would be exceeded (by a list, an unlock, or a new search's fee). | Raise the limit in the dashboard, or wait for next month. |
estimate_changed | 409 | The price changed since the estimate. | Make a new estimate. |
not_awaiting_confirmation | 409 | This estimate was already used. | Read the list with GET /v1/lists/{id}. |
quote_changed | 409 | The unlock price changed since the quote. | Get the quote again and show it before unlocking. |
nothing_to_unlock | 409 | The list has no checked leads left to unlock. | Read the list; the leads may already be unlocked. |
nothing_left | 409 | The list has no rest to check: it did not stop at its cap, the search ran dry, or its results were deleted. | Make a new list. |
already_continued | 409 | A follow-up of this list already started. | Open the follow-up list instead. |
results_expired | 409 | A follow-up list cannot start: the earlier list's results were deleted after 30 days. | Start a new list instead. |
not_finished | 409 | The list is still running. | Check again in a minute. |
expired | 410 | The results were deleted 30 days after the list finished. | Make a new list. |
invalid_link | 403 | The download link is not valid. | Ask for a new link. |
link_expired | 410 | The download link is older than 24 hours. | Ask for a new link. |
rate_limited | 429 | More than 60 requests in a minute, or more than 10 estimates or suggestions in a minute. The message says which. | Wait the seconds in Retry-After, then try again. |
estimate_limit | 429 | The account used its 100 estimates for today (UTC). | Wait the seconds in Retry-After, which runs to midnight UTC. |
daily_limit | 429 | The daily limit for new lists was reached. | Wait the seconds in Retry-After, which runs to midnight UTC. |
suggest_failed, provider_error | 502 | A data provider failed. | Try again soon. |
suggest_unavailable | 503 | Category suggestions did not answer within 15 seconds. | Try again, or search categories by name. |
provider_budget, screening_unavailable, unavailable | 503 | We are at capacity, could not check the request, or downloads are off. | Try again later. If downloads are off, read the leads instead. |
internal_error | 500 | Something went wrong on our side. | Try again. If it keeps happening, contact support. |
Limits
- 60 requests a minute per account, shared by all its keys and assistants. Over it you get
429 rate_limitedandRetry-After: 60. We send noX-RateLimitheaders: wait forRetry-After. - 100 estimates a day per account (UTC), across keys and assistants. Estimates and category suggestions are each also limited to 10 a minute, the same limits the website has. A refused estimate does not count.
- Leads come 50 at a time by default, at most 200. An assistant's
get_leadsdefaults to 25, at most 100. - Estimates ask for 10 to 10,000 leads, or
"all". An account that never paid estimates free while it has free leads; otherwise a new search costs $0.05, given back when its list starts (see the search fee). A list holds its most-possible cost from your balance. - Results are kept 30 days after a list finishes. CSV links work for 24 hours.
- Keys: up to 10 active API keys per account.
- Spending limit: you can cap each key or assistant connection per month. Leave it empty for no cap.
Questions? Email nadhim@leadmaster.tech.