Integration guide

LeadMerlin + Python

Plain HTTP (requests)

A single script using the requests library: resolve a city, start a search, poll, page through results and write a CSV.

Before you start

Setup

  1. 1

    Run the script

    Save as leads.py and run python leads.py.

    import os, sys, time, csv, requests
    
    API = "https://api.leadmerlin.com/api"
    H = {"x-api-key": os.environ["LEADMERLIN_API_KEY"]}
    
    def call(method, path, **kw):
        r = requests.request(method, f"{API}{path}", headers=H, timeout=30, **kw)
        if r.status_code == 402:
            sys.exit(f"Out of lead credits: {r.json()}")
        r.raise_for_status()  # 401 bad key, 400 bad request, 5xx server error
        return r.json()
    
    # 1. Resolve a city to a location code
    loc = call("GET", "/v2/locations", params={"q": "austin"})
    if not loc["locations"]:
        sys.exit("No matching location")
    code = loc["locations"][0]["location_code"]
    
    # 2. Start a search job ONCE (don't retry a POST blindly: it starts a second paid job)
    job = call("POST", "/v2/searches", json={
        "keyword": "roofing contractor",
        "location_codes": [code],
        "strictness": "moderate",
        "max_leads": 50,  # hard budget for this run
    })
    job_id = job["job_id"]
    
    # 3. Poll until the job finishes; completed and failed are both final
    deadline = time.time() + 15 * 60
    while True:
        status = call("GET", f"/v2/jobs/{job_id}")
        if status["status"] == "failed":
            sys.exit(f"Job failed: {status['error']}")
        if status["status"] == "completed":
            break
        if time.time() > deadline:
            sys.exit(f"Still running after 15 min; check job {job_id} later")
        print(f"{status['status']} {status['progress']}%")
        time.sleep(5)
    
    # 4. Page through results (requests URL-encodes the cursor)
    businesses, cursor = [], None
    while True:
        params = {"limit": 200, **({"cursor": cursor} if cursor else {})}
        page = call("GET", f"/v2/jobs/{job_id}/results", params=params)
        businesses += page["businesses"]
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]
    
    # 5. Write a simple CSV (guard against spreadsheet formulas in scraped text)
    def cell(v):
        v = "" if v is None else str(v)
        return "'" + v if v[:1] in ("=", "+", "-", "@") and not v.replace("+", "").replace(" ", "").isdigit() else v
    
    with open("leads.csv", "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["name", "owner", "email", "phone", "phone_type", "website", "city"])
        for b in businesses:
            w.writerow([cell(x) for x in (b["name"], b["owner"], b["email"], b["phone"],
                                          b["phone_type"], b["website"], b["contact"]["city"])])
    print(f"Saved {len(businesses)} leads ({status['over_budget_count']} more matched beyond the budget)")

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.