Integration guide

LeadMerlin + Node.js

Plain HTTP (fetch)

A dependency-free ES module using Node 18+ built-in fetch.

Before you start

Setup

  1. 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

API at a glance

CallWhat it does
GET /v2/locations?q=City name → location_code (free)
GET /v2/suggest?niche=Vague niche → Google categories + sharper keywords (free)
POST /v2/searchesStart a search job: keyword, 1–5 location_codes, optional strictness
POST /v2/enrichmentsEnrich up to 100 domains you already have
GET /v2/jobs/:idPoll until status is completed
GET /v2/jobs/:id/resultsPaginated 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

Get your API key.

Sign up free to get an lm_ key and 100 enriched leads. No credit card.