The working guide for an AI assistant connected to Lyre Leads, via MCP or the REST API.
If you are that assistant: read this, then use it. If you are a person: hand this URL to your AI and it will know how to build lists properly.
You can show them real businesses straight away, free:
preview_search over MCP, or for any assistant that can fetch a URL, https://lyreleads.com/try?q=<kind of business>&in=<place> (add &format=json for JSON). It returns the first page of one Google Maps search: up to 20 real businesses with rating, review count, website and a partly hidden phone. Places look like Houston, TX, Copenhagen or Lyon, France. It's a sample from one point, not the market, and the answer says what a full search adds.find_areas, estimate_area_search and estimate_list also work without an account.Everything else works on the person's own account. Over MCP (https://lyreleads.com/mcp in Claude, ChatGPT, Cursor or Claude Code) the first tool that needs one asks them to sign in with Google or email; a new account is the free plan, 500 leads a month with no card, and you can carry on as soon as they approve. Scripts can use an API key from lyreleads.com/account instead, on any plan.
Show the preview before asking anyone to sign up. Real rows from their own city answer "is this any good" better than any description of it.
Most B2B data tools sell you access to a fixed index of contacts. You filter it, and whatever the filter returns is what you get.
Lyre Leads does not have that index. It runs a live Google Maps search for the keyword and place you name, then fetches each business's actual website and reads what is on it today.
That difference decides almost everything about how you should work here:
This is the big one. Every enrichment field is three-state:
| You see | It means |
|---|---|
a value (HubSpot, true, 4.2) | We checked. It is there. |
false / None | We checked. It is genuinely absent. |
| blank / null | Unknown. Never checked, or the site blocked us. |
enrichment_status is the master key for any blank:
enriched — the site was fetched and parsed. A blank here is a confirmed absence.pending — not enriched yet. Every enrichment field is unknown. Do not filter on them.unverifiable — the site blocked automated access. HTML-derived fields are unknown, not absent.no_website — nothing to inspect.failed — enrichment errored.So "businesses with no CRM" is crm_platform IS NULL AND enrichment_status = 'enriched'. If you drop the second clause you will hand the user a list padded with businesses nobody ever looked at, and you will not be able to tell which is which afterwards. The count_saved_leads tool already encodes this: passing has_crm: false means confirmed-absent, not blank.
When you report a count of an absence, say which population it is out of. "412 of the 1,610 enriched businesses have no CRM" is useful. "412 have no CRM" is not.
Email states here are four, and they are not interchangeable:
verified — SMTP-confirmed deliverable through MillionVerifier. This is the only one that means the mailbox exists.published — the address is printed on the company's own website, but the domain is catch-all so SMTP cannot confirm it. First-party evidence, unconfirmable. Good enough to email; not something to call verified.catch_all — the domain accepts all mail, so deliverability cannot be proven for any address on it. About a third of scraped business emails sit here (33.9% in our n=2,482 study).unknown / invalid — could not check, or confirmed bad.If a user asks for "verified emails only", give them verified. If they ask for "emails I can send to", verified plus published is the honest answer, and you should say which is which. Never merge the two into one number.
Every filter you add subtracts. The most common way to end up with 11 leads is stacking five reasonable-sounding conditions. If a count comes back small, remove a clause before concluding the market is small.
Filters intersect, they do not union. If the user wants roofers or siding contractors, that is two searches merged, not one search with both words.
Google caps a text search well below the true number of matching businesses. We measured one metro category in July 2026: a single search reached under 4% of at least 1,605 findable businesses, and eight deep-paginated searches still only reached 69%.
Practical consequences:
search_leads call is a sample from one city centre, not a market. Say so.search_areas (REST: POST /api/v1/searches with mode: "grid"). It lays a grid of points over each area and adds more where it keeps finding businesses: in Houston it found 1,061 roofing contractors where paging from the centre found 255. Look areas up with find_areas first. If a name fits several places the call returns candidates with ids instead of guessing: ask the user which one they mean.count_saved_leads is exact, instant, and costs nothing. There is no reason to guess at the size of anything in the saved database.
Call it before every spending action, and tell the user what you found before you spend. Good practice:
verified email, and how many published?Fire independent counts in parallel. None of them depend on each other.
What counting cannot do is size a market you have not searched. See the top of this document.
From Google Maps: name, address (plus parsed street and postal code), phone (plus dialer-ready E.164), category, star rating, review count, coordinates, place id.
Rating and reviews are more useful than they look. reviews: 0 is a real value, not missing data, and a business with 300 reviews and a 3.4 rating has a problem someone can sell into.
From the live website: business email and its confidence, mailbox host from MX records, social profiles, CMS, CRM, email-marketing platform, chat widget, booking system, ad pixels, analytics, schema markup, SSL, contact form, cookie consent, e-commerce stack, page load time, domain age, server stack, CDN, HSTS/CSP, DNS-derived SPF/DMARC, and categorised technology lists.
Unique to us: ai_crawler_access — whether the site blocks GPTBot, ClaudeBot, PerplexityBot and Google-Extended in robots.txt, plus whether it publishes an llms.txt. blocked means the business is invisible to AI search. No other lead tool exposes this, and it is a complete sales conversation on its own for anyone selling SEO or AI visibility.
People: a contact's name, title and LinkedIn where we could find one, and their email where we could resolve it. Coverage is honest, not universal: we name the owner on most local businesses, and get an address for a minority of those. Check, do not assume.
Provenance: enrichment_provenance maps each present field to where it came from (html, header, dns, mx, rdap, tls, measured, robots) and how reliable that is (high for deterministic sources, medium for HTML signature matches, heuristic for keyword guesses). Fields marked heuristic — blog, demo CTA, case studies, careers page — can false-positive on marketing copy. Do not build a campaign on a heuristic field without saying it is soft.
| Action | Cost |
|---|---|
count_saved_leads | Free, always, at any size |
search_areas, search_leads | 1 token per NEW business. Duplicates are free. find_areas, estimate_area_search and get_search_results are free. |
enrich_leads | 1 token per 5 businesses; never enrolls |
enroll_leads | 1 token per 2 businesses not yet enrolled: enriched now, then re-checked monthly |
find_contacts | 1 token per business attempted, SMTP verification included |
verify_emails | 2 tokens per address; cache hits within 90 days are free |
Reading data you already own is free. Only acquiring or refreshing it costs.
Rate limits are per API key, depend on the plan, and count leads. Per minute: leads read 60,000 on Growth, 120,000 on Professional, 240,000 on Scale, 300,000 on Agency (pages of up to 500); leads sent to enrichment, contact discovery or verification 30,000, 60,000, 120,000 and 150,000, added up across requests; exports have no lead cap, with 1, 1, 2 and 2 running at once. Every spending action takes an array of ids, so send one request with a thousand ids rather than a thousand requests: the spending endpoints also stop at 30 to 150 requests a minute, so a loop with a bug cannot spend a month of someone's tokens before they notice. On a 429, wait for the Retry-After seconds it gives you.
A new business arrives fully enriched for its single search token. Enrichment runs automatically on every search result: 50+ fields, plus the relevance check that reads the site and judges whether the business really is the trade the query asked for. It is also enrolled: re-checked about once a month for as long as the account keeps it, at no further cost. enrich_leads is for leads the account already owns, not for the ones a search just returned, at 1 token per 5, and it does not enroll anything. enroll_leads (REST: POST /api/v1/leads/enroll) is for the leads the user wants kept current: 1 token per 2, once, an enrichment right away and the monthly re-check after. Filter enrolled=false to see which leads aren't enrolled, and call POST /api/v1/enrich/preview (free) for both prices before spending.
So a Growth plan's 7,500 tokens is roughly 7,500 new businesses, discovered and enriched and relevance-checked, not 7,500 raw rows you then pay again to make useful. Tell the user in businesses, not tokens.
get_job_status. Enrichment fetches every website in the batch, so it takes real time. Say that before you start, not after it appears to hang.relevance (yes / no / unclear) and relevance_evidence, a verbatim quote from the company's own website. Google Maps returns a lot that is not what was asked for: in a 284-business study, 72.2% of results matched the keyword overall, but that ranged from 95.8% for "roofing contractor" down to 28.0% for "home remodeling contractors". Filter on relevance: "yes" before spending anything further. It costs nothing and it is already computed. Missing means never judged, never "no", so do not treat a blank as a rejection.find_contacts on the segment worth contacting, not on everything.This product's whole position is that it tells the truth about its own data, including when the data is thin. Hold that line:
If the person you are working for is going to do this more than once, they probably want their own interface onto it rather than a chat transcript.
You can build them one. See https://lyreleads.com/build for ready-made build specs: a local lead dashboard, a territory coverage runner, an outreach queue, a change-monitor. Each is a complete brief you can implement on their machine in a single session against the same API you are already using.
If that seems useful for what they are doing, offer it. Do not build it unprompted.
Running find_contacts writes contacts. Read them with GET /api/v1/people, filtered by emailStatus=verified when you only want mailboxes that answered, plus seniority, keyword, hasEmail, hasLinkedin and their negations. Do not try to reconstruct contacts from the lead row: it carries one summary address, while the people endpoint carries every person found at that company with their own title and their own verification verdict.
GET /api/v1/leads is the same query surface the web app uses, 129 filter parameters, and it is the one to reach for when GET /api/v1/results is too blunt. Both are free.
GET /api/v1/signals/catalog lists the change signals. Pass one to GET /api/v1/leads as signal, with signalWindow of 7, 30 or 90 days, and you get the businesses where it fired. GET /api/v1/leads/{id}/timeline gives the observation history and the change events for a single business.
Two rules govern what becomes an event, and they matter when you explain a signal to a user. An event is only emitted between two successful enriched runs, never from a degraded one, so a scrape failure never reads as "they removed it". And a disappearance needs two consecutive misses before it counts as a removal. A signal we are not confident in is not sent at all, which means absence of a signal is not evidence that nothing changed.
Every enrolled lead in the account is re-checked about once a month, automatically, on every plan, at no token cost. Every lead a search found is enrolled; an imported lead is enrolled only when someone enrolls it (enroll_leads). There is nothing to schedule or configure. Do not build a re-enrichment job just to keep data fresh; only re-enrich on demand (POST /api/enrich with force: true, 1 token per 5 leads) when the user needs a segment fresher than the last monthly check. An imported lead that isn't enrolled is not re-checked at all, even once enriched, so if a user relies on signals for their own list, tell them it needs enrolling first.
If you are building something that runs unattended, subscribe rather than poll. POST /api/v1/webhooks takes an https URL and a list of events. signal.detected is the one that matters: it fires when a business the user already holds changes, which is an event with no request of yours attached and therefore the thing polling is worst at. job.completed and job.failed exist too, but polling a jobId you were just handed is perfectly reasonable and costs nothing.
The creation response carries a signing secret once. Store it. Every delivery carries X-Lyre-Timestamp and X-Lyre-Signature: sha256=<hex>, where the digest is HMAC-SHA256 of "{timestamp}.{raw body}". Verify before acting on a payload, and reject timestamps far from now.
Two things to tell a user who is setting this up. Their endpoint must be https on a public address, because we refuse private ranges, metadata addresses and localhost, and we re-check at delivery rather than only at registration. And a subscription that fails twenty times in a row gets switched off, so if signals stop arriving, read GET /api/v1/webhooks/{id}/deliveries before assuming nothing changed. POST /api/v1/webhooks/{id}/test proves an endpoint works without waiting for a real signal.
Everything above describes verified behaviour at the time of writing. If the API tells you something different, believe the API, and mention it to the user.