Leads
The Leads API searches Warmerly's crawled company database and lets you export matches to CSV or push them straight into a campaign's audience. It's scoped to your user account (not a project) — search results and export quota are shared across every project you own. Suppressed companies are always excluded, at search time and again at export/push time.
All endpoints below require X-Api-Key and do not read X-Project-Id.
The company object
{
"id": "b1e6a2c4-9f3d-4a7e-8c1b-2d5f9a3e7c6b",
"domain": "northwindtraders.com",
"companyName": "Northwind Traders",
"legalName": "Northwind Traders Ltd",
"description": "Wholesale importer of specialty foods.",
"websiteUrl": "https://northwindtraders.com",
"country": "GB",
"city": "Manchester",
"region": "Greater Manchester",
"streetAddress": "14 Dock Street",
"postalCode": "M1 2AB",
"phone": "+441611234567",
"orgType": "Wholesaler",
"industry": null,
"techStack": ["shopify", "klaviyo"],
"emailProvider": "google",
"linkedinUrl": "https://www.linkedin.com/company/northwind-traders",
"twitterUrl": null,
"socialUrls": ["https://www.facebook.com/northwindtraders"],
"contactEmail": "info@northwindtraders.com",
"allEmails": ["info@northwindtraders.com", "sales@northwindtraders.com"],
"crawledAt": "2026-08-10T04:22:00.000Z"
}
contactEmail is the first of allEmails, kept separate because export and
push-to-campaign both key off a single address per company. Deliverability signals
(MX/SPF/DMARC) are deliberately not exposed on this object — they're Warmerly's own
outreach data, not customer-facing.
Search companies
GET /leads/search
| Query param | Type | Required | Description |
|---|---|---|---|
q | string (max 200) | No | Full-text search across company name, description, and other indexed fields. |
country | string (2-letter) | No | ISO country code, e.g. GB. |
category | string | No | One or more categories, comma-separated (e.g. Restaurant,CafeOrCoffeeShop). |
provider | string | No | Email provider segment, e.g. google, microsoft. |
tech | string | No | One or more technologies, comma-separated (e.g. shopify,klaviyo) — matches companies using any of them. |
channel | "email" | "form" | "linkedin" | No | Which contact channel a company must have. Defaults to email — a company with no published contact address is never returned unless you ask for a different channel. form returns companies publishing a contact form, linkedin those with a company page. There is no value that removes the requirement. |
hasPhone | "true" | No | Only companies publishing a telephone number. |
hasCompanyNumber | "true" | No | Only companies publishing a Companies House registration number — in practice, UK-registered. |
hasVatNumber | "true" | No | Only companies publishing a VAT number. |
hiring | "true" | No | Only companies linking a careers page or stating they are hiring. |
hasContactForm | "true" | No | Only companies publishing a contact form URL. |
city | string (max 120) | No | Town or city, matched through the same text index as q. |
employeeBand | string | No | One coarse size band, e.g. 11-50. |
foundedFrom / foundedTo | integer | No | Inclusive founded-year range. |
cursor | string (UUID) | No | Pass the previous response's nextCursor to fetch the next page. |
limit | integer (1–100) | No | Page size. Default 50. |
curl "https://app.warmerly.com/api/v1/leads/search?country=GB&category=Restaurant&tech=shopify&limit=25" \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
"rows": [
{
"id": "b1e6a2c4-9f3d-4a7e-8c1b-2d5f9a3e7c6b",
"domain": "northwindtraders.com",
"companyName": "Northwind Traders",
"country": "GB",
"contactEmail": "info@northwindtraders.com",
"techStack": ["shopify", "klaviyo"]
}
],
"nextCursor": "b1e6a2c4-9f3d-4a7e-8c1b-2d5f9a3e7c6b",
"countLabel": "312",
"coverage": {
"sampleSize": 312,
"exact": true,
"email": 312,
"phone": 190,
"companyNumber": 141,
"contactForm": 88,
"linkedin": 97,
"country": 305
}
}
coverage reports how many of the matching companies carry each high-value
field, so you can see how complete a segment is before you export it. It is
measured over at most 1,000 matching rows: when exact is true the sample was
the entire result set and these are counts; when it is false more rows match
than were measured and the figures are a sampleSize-row sample, not totals.
The whole table is never aggregated.
countLabel is an exact count below 1,000 matches, and the literal string "1000+"
above that — matching results are never counted with an unbounded COUNT(*) over the
full table. nextCursor is present whenever there may be more rows; keep paging with it
until a response returns fewer rows than limit.
This endpoint has its own per-user throttle, separate from your API key's per-minute rate limit — see Rate limits below.
Database size
GET /leads/totals
How many companies the lead database currently holds. The crawl adds rows continuously, so these figures move.
{ "emailable": 4677084, "total": 13914447, "approximate": true, "asOf": "2026-08-21T15:41:02.000Z" }
| Field | Description |
|---|---|
emailable | Companies with a published contact email that are not suppressed — the population GET /leads/search draws from by default. |
total | All company records held, contactable or not. |
approximate | Always true. |
asOf | When the snapshot was taken. |
Both figures are estimates, derived from the database's own planner
statistics rather than by counting rows, and may be a few minutes behind on a
table this size. Treat them as "about N", never as an exact count. Either
figure can be null if the statistic is unavailable.
Export to CSV
POST /leads/export
Exports matching companies as a CSV file (download, not JSON). Debits your monthly lead export quota by the number of rows actually delivered — not the number you asked for.
Select rows with one of two shapes in the request body:
| Field | Type | Required | Description |
|---|---|---|---|
domains | string[] (1–10,000) | One of domains/selectAllMatching | Explicit list of domains to export. |
filters | object | No | Same filter shape as search (q, country, category, provider, tech, channel, hasPhone, and the firmographic filters). Booleans are JSON booleans here, not the strings the query string uses. Used with selectAllMatching. The channel default applies here too: an export never contains a company you cannot contact by email unless you named another channel. |
selectAllMatching | true | One of domains/selectAllMatching | Export every row matching filters, re-resolved server-side. |
A single request is capped at 10,000 rows regardless of plan, whichever selection method you use.
curl -X POST https://app.warmerly.com/api/v1/leads/export \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "filters": { "country": "GB", "tech": ["shopify"] }, "selectAllMatching": true }' \
-o leads.csv
Returns 200 with content-type: text/csv. The response also carries an
X-Lead-Count header with the exact row count in the file (CORS-exposed, so browser
fetch() can read it) — this can be lower than what you selected, because suppression
and the "has an email" filter are re-applied at export time. CSV columns, in order:
domain, company_name, legal_name, website_url, country, city, category, tech_stack,
email, all_emails, phone, street_address, region, postal_code, linkedin_url,
twitter_url, other_social, mail_provider, description, last_checked
New columns may be appended in future; existing columns won't be reordered or removed.
Errors: 400 bad_request (no_selection if neither domains nor selectAllMatching
is set, invalid_export_request for a malformed body), 402 payment_required if the
request would exceed your monthly export quota:
{
"error": {
"code": "payment_required",
"message": "Lead export limit reached: 480/500 used this month.",
"details": null
}
}
Push to a campaign
POST /leads/to-campaign
Same selection shape as export, plus a target campaign — adds matching companies directly to a campaign's audience instead of downloading a file. Debits the same monthly export quota as CSV export (the two share one meter).
| Field | Type | Required | Description |
|---|---|---|---|
campaignId | string (UUID) | Yes | The campaign to add leads to. |
domains | string[] (1–10,000) | One of domains/selectAllMatching | Explicit list of domains to add. |
filters | object | No | Same filter shape as search. Used with selectAllMatching. |
selectAllMatching | true | One of domains/selectAllMatching | Add every row matching filters. |
curl -X POST https://app.warmerly.com/api/v1/leads/to-campaign \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"campaignId": "3a6f9e2e-4b7a-4e9a-9b1d-6e3a2f8c1a10",
"domains": ["northwindtraders.com"]
}'
{ "added": 1, "skipped": 0 }
skipped counts rows that were dropped because they had no contact email or were
already in the campaign's audience (deduplicated by lowercased email) — those don't
count against your quota. Errors mirror export's 400/402 cases,
plus 404 not_found for an unknown campaignId and 402 billing_locked if the
campaign's workspace has a billing issue.
Export quota
GET /leads/quota
Returns your current lead-export usage for the billing period. A pure read — checking it never debits quota.
curl https://app.warmerly.com/api/v1/leads/quota \
-H "X-Api-Key: wmv_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{ "used": 480, "limit": 500, "remaining": 20 }
Rate limits & errors
GET /leads/search has its own throttle on top of your API key's per-minute limit:
120 requests per 5 minutes, per user (not per key or IP). Exceeding it returns
429 too_many_requests. See Errors & Rate Limits for the shared error
envelope and your key's general per-minute limit.
