Integration guide
LeadMerlin + Node.js
Plain HTTP (fetch)
A dependency-free ES module using Node 18+ built-in fetch.
Before you start
- Node.js 18 or newer.
- LEADMERLIN_API_KEY set in your environment.
Setup
- 1
Run the script
Save as leads.mjs and run node leads.mjs.
// Node 18+ (built-in fetch). Save as leads.mjs and run: node leads.mjs const API = "https://api.leadmerlin.com/api"; const H = { "x-api-key": process.env.LEADMERLIN_API_KEY, "content-type": "application/json" }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function call(path, init = {}) { const res = await fetch(API + path, { ...init, headers: H, signal: AbortSignal.timeout(30_000) }); const body = await res.json().catch(() => ({})); if (res.status === 402) throw new Error("Out of lead credits (balance " + body.balance + ")"); if (!res.ok) throw new Error("HTTP " + res.status + ": " + (body.error ?? res.statusText)); return body; } // 1. Resolve a city const { locations } = await call("/v2/locations?q=" + encodeURIComponent("austin")); if (!locations.length) throw new Error("No matching location"); const code = locations[0].location_code; // 2. Start a search job ONCE (a blind POST retry would start a second paid job) const job = await call("/v2/searches", { method: "POST", body: JSON.stringify({ keyword: "roofing contractor", location_codes: [code], max_leads: 50 }), }); // 3. Poll; completed and failed are both final const deadline = Date.now() + 15 * 60_000; let status; for (;;) { status = await call("/v2/jobs/" + job.job_id); if (status.status === "failed") throw new Error("Job failed: " + status.error); if (status.status === "completed") break; if (Date.now() > deadline) throw new Error("Still running after 15 min; check job " + job.job_id + " later"); console.log(status.status, status.progress + "%"); await sleep(5000); } // 4. Page through results const businesses = []; let cursor = null; do { const qs = new URLSearchParams({ limit: "200", ...(cursor ? { cursor } : {}) }); const page = await call("/v2/jobs/" + job.job_id + "/results?" + qs); businesses.push(...page.businesses); cursor = page.has_more ? page.next_cursor : null; } while (cursor); console.log(businesses.length, "leads;", status.over_budget_count, "more matched beyond the budget"); console.log(businesses.slice(0, 3).map((b) => ({ name: b.name, owner: b.owner, email: b.email })));
Location codes in these examples are placeholders — always look yours up with GET /v2/locations first.
Tips
- Want to try without a key? Swap the paths for /v2/demo/* — they return fixture data and need no auth.
- Quick check from the terminal: curl -X POST "https://api.leadmerlin.com/api/v2/demo/searches" \ -H "content-type: application/json" \ -d '{"keyword":"plumber","location_codes":[1026201]}'
API at a glance
| Call | What it does |
|---|---|
| GET /v2/locations?q= | City name → location_code (free) |
| GET /v2/suggest?niche= | Vague niche → Google categories + sharper keywords (free) |
| POST /v2/searches | Start a search job: keyword, 1–5 location_codes, optional strictness |
| POST /v2/enrichments | Enrich up to 100 domains you already have |
| GET /v2/jobs/:id | Poll until status is completed |
| GET /v2/jobs/:id/results | Paginated enriched businesses (cursor, limit ≤ 200) |
| /v2/demo/* | Same shapes on fixture data — no key, no credits |
All calls use the x-api-key header except the demo endpoints. Base URL: https://api.leadmerlin.com/api
Related
Use cases
Other integrations
Get your API key.
Sign up free to get an lm_ key and 100 enriched leads. No credit card.