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 paramTypeRequiredDescription
qstring (max 200)NoFull-text search across company name, description, and other indexed fields.
countrystring (2-letter)NoISO country code, e.g. GB.
categorystringNoOne or more categories, comma-separated (e.g. Restaurant,CafeOrCoffeeShop).
providerstringNoEmail provider segment, e.g. google, microsoft.
techstringNoOne or more technologies, comma-separated (e.g. shopify,klaviyo) — matches companies using any of them.
channel"email" | "form" | "linkedin"NoWhich 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"NoOnly companies publishing a telephone number.
hasCompanyNumber"true"NoOnly companies publishing a Companies House registration number — in practice, UK-registered.
hasVatNumber"true"NoOnly companies publishing a VAT number.
hiring"true"NoOnly companies linking a careers page or stating they are hiring.
hasContactForm"true"NoOnly companies publishing a contact form URL.
citystring (max 120)NoTown or city, matched through the same text index as q.
employeeBandstringNoOne coarse size band, e.g. 11-50.
foundedFrom / foundedTointegerNoInclusive founded-year range.
cursorstring (UUID)NoPass the previous response's nextCursor to fetch the next page.
limitinteger (1–100)NoPage 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" }
FieldDescription
emailableCompanies with a published contact email that are not suppressed — the population GET /leads/search draws from by default.
totalAll company records held, contactable or not.
approximateAlways true.
asOfWhen 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:

FieldTypeRequiredDescription
domainsstring[] (1–10,000)One of domains/selectAllMatchingExplicit list of domains to export.
filtersobjectNoSame 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.
selectAllMatchingtrueOne of domains/selectAllMatchingExport 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).

FieldTypeRequiredDescription
campaignIdstring (UUID)YesThe campaign to add leads to.
domainsstring[] (1–10,000)One of domains/selectAllMatchingExplicit list of domains to add.
filtersobjectNoSame filter shape as search. Used with selectAllMatching.
selectAllMatchingtrueOne of domains/selectAllMatchingAdd 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.