Lyre Leads API

Integrate AI-powered Google Maps lead generation directly into your applications, AI agents, LangChain pipelines, and automation workflows. Search businesses, retrieve 50+ company-level data points per lead, including verified emails, AI-discovered contacts with personal LinkedIn profiles, and 1,519 tech stack detections, each one judged against the query you searched for. All via a simple REST API.

✓ REST API ✓ OpenAPI 3.0 ✓ LangChain / AI Agent Ready
ℹ️ API access is on every plan, Free included. Generate your API key in Account → API Keys. Assistants don't need a key: they connect to /mcp with sign-in.

Quick start

Get your first API call working in under 2 minutes.

1

Get an API Key

Log in → Account → API Keys → Generate Key. Save it immediately, shown once.

2

Check Your Balance

Call GET /api/v1/balance with your Bearer key to confirm it works.

3

Search for Leads

Call POST /api/v1/search with query, city, and country.

4

Keep what matched

Every enriched row carries relevance and the quote behind it. Filter on "yes".

First API call

cURL
curl https://lyreleads.com/api/v1/balance \
  -H "Authorization: Bearer lyre_your_api_key_here"
Python
import requests

API_KEY = "lyre_your_api_key_here"
BASE_URL = "https://lyreleads.com"
headers = {"Authorization": f"Bearer {API_KEY}"}

resp = requests.get(f"{BASE_URL}/api/v1/balance", headers=headers)
print(resp.json())
# {'tier': 'Growth', 'leads_balance': 6800, 'leads_monthly_allocation': 7500, ...}
Node.js
const res = await fetch('https://lyreleads.com/api/v1/balance', {
  headers: { 'Authorization': 'Bearer lyre_your_api_key_here' }
});
const data = await res.json();
console.log(data.tokens_balance); // 4500

Authentication

All API v1 requests require an API key sent as a Bearer token in the Authorization header:

HTTP Header
Authorization: Bearer lyre_<your-64-hex-key>

API keys are scoped to your account. They inherit your lead balance and tier permissions. You can create up to 10 active keys per account and revoke them at any time from the Account page.

⚠️ Your API key is shown only once at creation. Store it securely in environment variables or a secrets manager, never hard-code it in your application source.
ℹ️ Every plan can create keys, Free included. A key spends from your lead balance like the website does, and the Free plan's 500 leads a month are its real limit.

Error handling

The API uses standard HTTP status codes. All error responses are JSON with an error field:

StatusMeaningCommon cause
200OKRequest succeeded
400Bad RequestMissing required field or invalid parameter
401UnauthorizedMissing, invalid, or revoked API key
402Payment RequiredInsufficient leads, buy more or wait for the monthly reset
403ForbiddenPlan upgrade required, or account suspended
404Not FoundResource not found (model, result)
429Too Many RequestsYour plan's rate limit for that kind of request; wait the Retry-After seconds
500Server ErrorSomething went wrong on our end
Error response example
{
  "error": "Insufficient leads.",
  "leads_needed": 20,
  "leads_balance": 5,
  "upgrade_url": "/pricing"
}

Rate limits

Limits are per API key and depend on your plan. Two keys calling from the same cloud IP don't share a budget. Each window is one minute.

PlanLeads read a minuteLeads a minute into enrichment, contacts or verificationLeads per export
Growth60,00030,000Unlimited, 1 file at a time
Professional120,00060,000Unlimited, 1 file at a time
Scale240,000120,000Unlimited, 2 files at a time
Agency300,000150,000Unlimited, 2 files at a time

Reading covers the free endpoints: leads, people, count, results, areas, previews and webhook management. A page returns up to 500 leads, and the limit is 120 pages a minute on Growth, 240 on Professional, 480 on Scale and 600 on Agency. Enrichment, contacts and verification are metered by the ids you send, added up across requests, so one request with 3,000 ids and three requests with 1,000 each cost the same. Send ids in one array rather than looping: there is also a ceiling of 30 to 150 requests a minute by plan, so a runaway loop can't drain a month of leads before you notice. Your lead balance usually runs out long before either limit. Searches send areas rather than ids, so they only count against the request ceiling, and at most two run at once. Exports have no lead cap; what's limited is how many run at once, because a whole-database export holds the database for seconds.

Every response carries RateLimit and RateLimit-Policy headers, and the spending endpoints also send X-Spend-Leads-Limit and X-Spend-Leads-Remaining. A 429 carries Retry-After and a JSON body saying which limit you hit and when to try again, so n8n, Clay and your own scripts can back off without guessing.

One lead buys one new business, and that business arrives enriched and relevance-checked at no extra cost. Businesses you already hold are never charged twice. Every business a search finds is enrolled in enrichment, which means we re-check it about once a month for as long as you keep it, at no charge. Enriching a lead you already hold costs one token per five businesses, and it doesn't enroll it. Leads you import are only re-checked once you enroll them with POST /api/v1/leads/enroll: two businesses per token, once, with an enrichment right away. Check which is which with the enrolled filter, and get the exact price first, free, from POST /api/v1/enrich/preview. Contact discovery costs 2 tokens per named contact found, nothing where nobody turns up, with SMTP verification included, and standalone email verification is included with re-checks inside 90 days free. Check your balance at any time via GET /api/v1/balance.

MCP server (AI agents)

Lyre Leads ships a Model Context Protocol endpoint, so Claude, ChatGPT, Cursor, Claude Code and any MCP-compatible client can run grid searches over whole areas, enrich the results, find and verify contact emails, and read the relevance verdict on each result directly. It exposes fifteen tools: preview_search, find_areas, estimate_area_search, search_areas, get_search_results, search_leads, list_results, estimate_list, count_saved_leads, enrich_leads, enroll_leads, find_contacts, verify_emails, get_job_status and get_balance, backed by the same routes documented below (same auth, limits, and token costs).

count_saved_leads is free and exact at any size. Agents should size a segment before spending on it, so counting is never billed and never rationed.

Point your agent at lyreleads.com/for-agents first. It is a working guide written for an AI assistant rather than a human: what each field means, what a blank value does and does not tell you, where our coverage is weak, and the four mistakes that produce confidently wrong lead lists. Raw markdown at /for-agents.md.

Building a client on top of this API? lyreleads.com/build carries complete, implementable specs an AI coding agent can follow to build a local dashboard, territory runner, outreach queue or change monitor in about one session each.

Connecting is signing in. In Claude or ChatGPT, add https://lyreleads.com/mcp as a custom connector; in Claude Code run claude mcp add --transport http lyre-leads https://lyreleads.com/mcp. Nothing else to configure: the first tool that needs an account opens a sign-in with Google or email, and a new account is the free plan, 500 leads a month with no card. You can see and disconnect connected apps under Account, API Keys. Step-by-step setup for Claude, with prompts to try: lyreleads.com/claude.

Four tools need no account at all: preview_search (the first page of one Google Maps search for any trade and place, as real rows), find_areas, estimate_area_search and estimate_list. The same preview is a plain URL any assistant or script can fetch: /try?q=roofing+contractor&in=Houston,+TX (add &format=json for JSON).

Scripts and clients without sign-in can send an API key as a bearer token instead:

{
  "mcpServers": {
    "lyre-leads": {
      "type": "http",
      "url": "https://lyreleads.com/mcp",
      "headers": { "Authorization": "Bearer lyre_your_key_here" }
    }
  }
}

Typical agent flow: find_areas to pin down the places (a shared name like Houston comes back as candidates with ids), estimate_area_search to size it for free, search_areas to run the grid, get_job_status with kind search until it's finished, then get_search_results with {"relevanceYes": true} before you spend anything else on the batch. search_leads is the quick one-city search, answered in the same call.

Endpoints

Lead balance

GET /api/v1/balance Returns your current lead balance, allowance, rollover cap and reset dates

No request body required.

Response

JSON
{
  "tier": "Growth",
  "leads_balance": 6800,
  "leads_monthly_allocation": 7500,
  "leads_rollover_cap": 22500,
  "leads_reset_date": "2026-09-01T00:00:00.000Z",
  "leads_next_reset": "2026-10-01T00:00:00.000Z"
}

Search whole areas (grid)

POST /api/v1/search above pages outward from one city centre, which is quick for a small town and misses most of a metro. Paging deeper from one point keeps returning the same central neighbourhoods. The grid lays points across each area instead and adds more where it keeps finding new businesses. In Houston, for the same roofing query and the same spend, one centre returned 255 businesses and a 76-point grid returned 1,061. This is the search the Search page runs, over the same handler.

GET /api/v1/areas The places a search accepts, with ids. Free.
Query paramTypeDescription
countryrequiredstringFor example United States. Leave it out to get the list of countries.
qoptionalstringPart of a name, case-insensitive. Most populous first.
limitoptionalintegerDefault 20, max 100.
Response
{
  "country": "United States",
  "areas": [
    { "id": "houston-texas-771e0d", "name": "Houston", "state": "Texas", "population": 2304580, "area_km2": 1659.54 }
  ],
  "count": 1
}
POST /api/v1/searches/estimate How big a grid would be, before you run it. Free.

Same body as the search below. Returns seedPoints, alreadyCoveredAreas (areas your account already covers for this query, which a search skips) and a rough range of provider calls. It can't give a price, because you pay per new business found and nobody knows how many exist before looking.

POST /api/v1/searches Start a search over one or more areas. 1 token per new business.
ParameterTypeDescription
queryrequiredstringThe business you want, for example roofing contractor.
countryrequiredstringAs listed by GET /api/v1/areas.
areasrequiredarray"Houston, Texas", {"name", "state"}, or an area id.
modeoptionalgrid | citygrid covers the whole area. city pages from the centre and is cheaper for small towns. Default city.

A name several places share is never guessed. The US list has four places called Houston and thirteen called Springfield. If an area could be more than one of them, the request returns 400 with ambiguous[].candidates, each carrying an id, a label, its population and coordinates. Nothing starts and nothing is charged. Resend with the id you mean. An unknown name returns 400 with unknown[].suggestions.

Request and response
{ "query": "roofing contractor", "country": "United States",
  "areas": ["Houston, Texas", "Dallas, Texas"], "mode": "grid" }

{ "job_id": "srch-…", "mode": "grid",
  "status_url": "/api/v1/searches/srch-…",
  "results_url": "/api/v1/searches/srch-…/results" }

You pay one token per new business. A listing you already hold costs nothing, though fetching pages carries a small per-page minimum. At most two searches run at once per account, and sending the same search again while it runs returns the running job rather than starting a second. Every new business is enriched, relevance-checked and enrolled in the monthly re-check as it lands.

GET /api/v1/searches/{id} Progress. Poll every few seconds, or subscribe to job.completed.

Returns status, progress, completedCities of totalCities, found, and finished: true once it has stopped for any reason. DELETE /api/v1/searches/{id} stops a running search and keeps what it found.

GET /api/v1/searches/{id}/results The leads that search found, with every filter of /api/v1/leads. Free.

Pages of up to 500 leads, in the same shape as GET /api/v1/leads, and it takes the same filters, so ?relevanceYes=true&hasVerifiedEmail=true works here too.

Python, start to finish
job = requests.post(f"{BASE_URL}/api/v1/searches", headers=headers, json={
    "query": "roofing contractor", "country": "United States",
    "areas": ["Houston, Texas"], "mode": "grid"}).json()

while not requests.get(BASE_URL + job["status_url"], headers=headers).json().get("finished"):
    time.sleep(10)

page = requests.get(BASE_URL + job["results_url"] + "?limit=500&relevanceYes=true", headers=headers).json()
for lead in page["results"]:
    print(lead["company_name"], lead.get("phone"))

Retrieve past results

GET /api/v1/results Paginated access to your stored search results
Query paramTypeDescription
limitoptionalintegerResults per page (default 20, max 100)
offsetoptionalintegerPagination offset (default 0)
queryoptionalstringFilter by search query (partial match)
search_idoptionalintegerFilter by search history ID
cURL
curl "https://lyreleads.com/api/v1/results?limit=50&query=plumbers" \
  -H "Authorization: Bearer lyre_..."
Response
{
  "results": [ /* array of result objects */ ],
  "total": 2540,
  "limit": 50,
  "offset": 0,
  "has_more": true
}

Each result object contains enrichment data including email, facebook_url, instagram_url, linkedin_url, has_facebook_pixel, has_google_analytics, cms_platform, tech_stack (JSON array of every technology detected on the site, from a catalogue of 844 web technologies across 32 categories), plus categorized tech columns: tech_frameworks, tech_analytics, tech_advertising, tech_marketing, tech_ecommerce, tech_customer_tools, tech_infrastructure, and merchant_signals (what the site says: cash only, age gate, GLP-1, no refunds; blank on a lead read before 12 October 2026 means not yet read). Filter on them with merchantSignal, paymentProvider, their exclude twins and the one-click merchantView; every name, group and view is listed at GET /api/merchant-vocabulary.

Boolean signals: has_ad_pixels, has_marketing_automation, has_ecommerce, has_reviews_widget, has_scheduling, has_contact_form. Firmographics: state, domain_age_years, email_marketing_platform. AI visibility: ai_crawler_access (open/partial/blocked/unknown, whether the site's robots.txt blocks AI crawlers like GPTBot, ClaudeBot, PerplexityBot), ai_bots_blocked, has_llms_txt. Also includes chat_widget, booking_system, crm_platform, has_ssl, contact_name, contact_title, contact_linkedin, relevance, and relevance_evidence.

Search Match

GET /api/v1/results Is this business the trade you searched for? Included, no extra cost.

Google Maps returns businesses that are not what you asked for. Across 284 businesses we hand-checked, 72.2% of results matched the keyword, ranging from 95.8% for roofing contractor to 28.0% for home remodeling contractors.

So enrichment reads each company's own website and judges it against the query that row came from. The verdict ships with the sentence that justifies it, quoted from the page, so you can audit any call we make. This runs automatically on every enriched lead and costs nothing beyond the search lead.

FieldTypeDescription
relevancestringyes, no, unclear, or null. null means never judged (no website, unreadable page, or a row created without a query). It never means "no".
relevance_evidencestringThe phrase copied verbatim from the company's page that justifies the verdict.
Filter to what you actually asked for
// POST /api/v1/count, free at any size
{
  "filters": { "relevance": "yes", "has_email": true }
}

// A row from GET /api/v1/results
{
  "name": "Bergen Roofing & Sheet Metal",
  "query": "roofing contractor",
  "relevance": "yes",
  "relevance_evidence": "Residential and commercial roof replacement since 1994"
}

In a 284-business study we published, a model reading the company's website kept 98.8% of the genuine businesses, against 87.4% for filtering on the Google Maps category, which silently discards about one real business in eight. That measures the method rather than this exact check, and both raters in the study were language models, which is the weakness. We publish the study including the half that argues against us.

Running the whole thing without us

A subscription and an API key are the entire product. Everything the web app does to find, enrich and contact a business is reachable over HTTP, using the same handlers, the same filters and the same lead accounting the browser gets.

The loop

  1. POST /api/v1/count sizes a segment for nothing. Do this before anything that spends.
  2. POST /api/v1/searches sweeps whole areas with the grid as a job, and GET /api/v1/searches/{id}/results reads what it found. For one small town, POST /api/v1/search answers in the same request with result_ids. Duplicates you already own are skipped and not charged.
  3. POST /api/v1/leads/import brings in businesses you already have, free. Enriching them with POST /api/enrich costs five businesses per token; enrolling them in the monthly re-check with POST /api/v1/leads/enroll costs two businesses per token, once, enrichment included.
  4. POST /api/enrich/find-contacts takes those ids and returns named contacts with their mailboxes SMTP-verified, 2 tokens per contact found and nothing where nobody turns up.
  5. GET /api/v1/people reads the contacts back, filtered by emailStatus=verified, seniority, or any of fourteen other filters.
  6. GET /api/v1/leads queries everything you own across 129 filter parameters: relevance, verification state, enrichment status, detected CMS, CRM, chat widget, booking system, data freshness.
  7. GET /api/v1/export hands the same filtered segment back as a file for your warehouse.

Keeping it alive

A lead list starts ageing the moment you download it, so we re-check every enrolled lead in your account about once a month, on every plan, without spending leads and without you scheduling anything. Every lead a search finds is enrolled; an imported lead is enrolled when you enroll it (POST /api/v1/leads/enroll). These three endpoints tell you what the re-checks found.

  • GET /api/v1/signals/catalog lists the change signals we track: review surges, rating moves, technology added or dropped, hiring flips.
  • GET /api/v1/leads accepts signal and signalWindow, so "everything that changed in the last 30 days" is a query, not a diff you write yourself.
  • GET /api/v1/leads/{id}/timeline returns the observation history and the change events for one business, so you can show a customer what moved and when.

Events are only emitted between two successful enriched runs, never from a degraded one, and a disappearance needs two consecutive misses before it counts as a removal. A signal we are not sure about is not sent.

Getting pushed to instead of polling

Polling a job you started is fine. You have the jobId and you know it's running. Change signals are the awkward case: a landscaper installs online booking three weeks after you last looked, with no request of yours to hang a response on. Without a push you'd poll on a timer forever and mostly learn nothing.

POST /api/v1/webhooks subscribes an https endpoint to signal.detected, job.completed and job.failed. The response carries a signing secret, once. Every delivery then arrives with X-Lyre-Timestamp and X-Lyre-Signature: sha256=<hex>, the digest being HMAC-SHA256 of "{timestamp}.{raw body}" under that secret. Verify it before trusting a payload, and reject timestamps far from now so a captured delivery can't be replayed at you.

Failed deliveries retry five times with widening backoff, from one minute out to ten hours (about twelve hours in all), which is long enough to fix a broken endpoint and still receive the event. A subscription that fails twenty times consecutively is switched off rather than retried forever, and stays visible with its last error so you can see why. GET /api/v1/webhooks/{id}/deliveries is the log, and POST /api/v1/webhooks/{id}/test fires a synthetic event so you can prove the endpoint works before a real signal ever arrives.

Your URL has to be https on a publicly routable address. We refuse private and reserved ranges, cloud metadata addresses, localhost, and hostnames that resolve to any of those, and we re-check at delivery time as well as at registration, because a hostname can be repointed after you register it.

Every endpoint above is in the OpenAPI spec, and the fields they return are defined in the data dictionary.

OpenAPI spec

GET /api/v1/openapi.json OpenAPI 3.0 specification, no auth required

Returns a complete OpenAPI 3.0 spec. Use this to auto-configure LLM tool-use, Postman, Swagger UI, or any OpenAPI-compatible tooling. No API key required to fetch the spec.

LangChain example
from langchain.tools import OpenAPISpec, APIOperation
from langchain.chains import OpenAPIEndpointChain

spec = OpenAPISpec.from_url("https://lyreleads.com/api/v1/openapi.json")
# Agent can now auto-discover and call Lyre Leads endpoints