ListMatchGenie

API Reference

REST API for programmatic access to ListMatchGenie. Upload files, run matches, fetch reports, and export results — all from your own code or automation.

The ListMatchGenie REST API lets you run every stage of the pipeline from your own code: upload files, kick off match jobs, read the results and the computed report, and pull formatted PDF / XLSX / PPTX exports. Everything the app does, the API can do.

API access is a Business-plan feature. Generate a key under Account → API keys inside the app.

Concepts you need before your first call

Spend two minutes here. The rest of this page assumes you understand these five ideas.

1. Source file vs. master file

Every match job compares two files:

  • Source file — the list you're checking. New leads, a newsletter signup export, today's form submissions, a prospecting list. This is usually the smaller, fresher file.
  • Master file — the list you're checking against. Your CRM, your customer database, your existing contact list. The system of record.

Concrete example. You export 5,000 new leads from a landing page. You want to know which of them are already in your 500,000-row Salesforce export so you don't re-email existing customers.

  • Source = new_leads_may.csv (5,000 rows)
  • Master = salesforce_export.csv (500,000 rows)

The match produces, for each source row, either a pointer to a master row ("this lead is already your customer") or nothing ("genuinely new lead"). Your output CSV has every original source column plus the matched master columns and the match metadata — no VLOOKUPs required.

purpose in the API is either "source" or "master" and it's sticky per file: you decide when you upload. A single file can be used as either role across different jobs, but most users keep one stable master file and upload a new source file each run.

2. The lifecycle of a file

POST /files         → upload your bytes to S3    → file.status = "uploaded"
                      ↓ (worker auto-prepares)
GET  /files/{id}    → parses headers, profiles   → file.status = "preparing"
                      columns, writes cleanse report
                      ↓
                    → ready to use in a match    → file.status = "ready"

Files are ready when status === "ready". Don't start a match against a file still in preparing — the job will wait, but you burn time. Preparation takes seconds for small files, up to a minute for files in the millions of rows.

3. The lifecycle of a match job

POST /jobs          → job.status = "pending"    (queued)
                      ↓
                    → job.status = "processing" (worker picked it up)
                      ↓
                    → job.status = "completed"  (result CSV available)
                         or     "failed"        (errorMessage populated)

Polling cadence: every 2-5 seconds is plenty. Most jobs of under 100K source rows finish in under a minute.

4. Reports vs. raw results

Every completed match produces two output surfaces:

  • Raw result CSV — every source row + matched master columns + _lmg_match_status, _lmg_match_score, etc. Fetched via job.downloadUrl on GET /jobs/{jobId}.
  • Report — computed rollups (match rate, score distribution, data-quality stats, pivots by column) plus AI-written narrative sections. Fetched via GET /reports/{reportId}. One report per job.

If you just want the matched rows to import into your CRM: use the CSV. If you want dashboard-grade analytics or an exec-ready PDF: use the report.

5. Match profiles

A profile tells the matcher how to compare rows: which columns map to which (e.g. "their first_name column is our fname"), which fields are primary keys vs. supporting evidence, whether to treat this as people-matching or product-matching. Profiles are saved configurations — create them once in the app wizard, reference them by ID in API calls.

Today, profile creation is UI-only (the wizard walks through column mapping with previews). The API reads profile IDs but doesn't create them. If you need programmatic profile creation, email us.

Base URL

https://app.listmatchgenie.com/api/v1

All endpoints are under that prefix. Replace /api/v1 with nothing and you get the app's internal routes, which are session-auth only and not supported for integrations.

Authentication

Every request needs an Authorization header:

Authorization: Bearer lmg_live_YOUR_KEY_HERE

Keys are created from Account → API keys. The raw key is shown exactly once at creation — copy it into your secrets manager immediately. Lose it and you'll need to regenerate (which revokes the old key atomically).

Keys start with lmg_live_ followed by 32 url-safe base64 characters. If you see lmg_test_ anywhere, that's a mistake — we don't run separate test keys today. All keys hit the same production data.

Managing keys

ActionUI pathEffect
CreateAccount → API keys → New keyIssues a new key with a label. Shown once.
RegenerateAccount → API keys → RegenerateAtomic rotate. Old key dies immediately, new key shown once, same label & scope.
RevokeAccount → API keys → RevokeSoft delete. Requests with the key get 401 Invalid or revoked API key.
UsageAccount → API keys → Chart icon30-day request + error counts per key.

Rate limits

20 requests per 10 seconds per key.

Every response — success or error — includes three headers so your client can pace itself without sampling 429s:

X-RateLimit-Limit: 20
X-RateLimit-Remaining: 17
X-RateLimit-Reset: 1714237890
  • X-RateLimit-Limit — the cap for the window.
  • X-RateLimit-Remaining — calls still available. Drops to 0 when you've hit the cap.
  • X-RateLimit-Reset — Unix timestamp (seconds) when the window resets and Remaining climbs back to Limit.

On a 429 Too Many Requests you also get Retry-After as an integer number of seconds. Respect it — keep retrying immediately and you'll just keep hitting 429.

Need higher limits (e.g. bulk import)? Email support and we'll raise on a per-key basis.

OpenAPI spec

Machine-readable spec at:

https://app.listmatchgenie.com/api/v1/openapi.json

Point Postman, Insomnia, Stoplight, or any SDK generator at that URL.

Quickstart: your first API call

Verify your key works and see which tier you're on:

curl https://app.listmatchgenie.com/api/v1/whoami \
  -H "Authorization: Bearer $LMG_API_KEY"

Expected response:

{
  "user": {
    "id": "0ff8a06d-9e87-4abc-bd22-0e6a71084fa1",
    "email": "you@example.com",
    "name": "You"
  },
  "tier": "business"
}

If you get 401 Missing Authorization header — you forgot the -H. If you get 401 Invalid API key format — your key is malformed (probably copy-paste munged the lmg_live_ prefix). If you get 401 Invalid or revoked API key — the key was revoked or never existed.

Full end-to-end walkthrough

This is the complete path: upload a source file, upload a master file, pick a profile, run a match, pull the result, fetch the report, export a PDF. Every step is a real curl call you can copy.

export API="https://app.listmatchgenie.com/api/v1"
export AUTH="Authorization: Bearer $LMG_API_KEY"

Step 1 — Upload the source file

Two-step upload: request a presigned S3 URL, then PUT your bytes directly to S3. Bytes never pass through our servers.

# Request the presigned URL
curl -X POST "$API/files" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "fileName": "new_leads_may.csv",
    "fileSize": 524288,
    "contentType": "text/csv",
    "purpose": "source"
  }'

Response:

{
  "fileId": "b1c9f3a0-...",
  "uploadUrl": "https://s3.us-east-1.amazonaws.com/...presigned...",
  "s3Key": "users/.../raw.csv"
}

Now PUT the actual bytes:

curl -X PUT "$UPLOAD_URL_FROM_ABOVE" \
  -H "Content-Type: text/csv" \
  --data-binary @new_leads_may.csv

After the PUT completes, the file is automatically queued for preparation (header parsing, column profiling, cleanse report). Wait for status === "ready":

curl "$API/files/b1c9f3a0-..." -H "$AUTH"
# → { "file": { "status": "preparing", ... } }
# ...a few seconds later...
# → { "file": { "status": "ready", "rowCount": 5000, ... } }

Step 2 — Upload the master file

Same two-step flow, with "purpose": "master". In practice you upload your master file once and reuse it across many source files — there's no need to re-upload your CRM every run.

curl -X POST "$API/files" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "fileName": "salesforce_master.csv",
    "fileSize": 104857600,
    "purpose": "master"
  }'
# → presigned URL, PUT the bytes, poll /files/{id} for "ready"

Step 3 — Pick a match profile

Profiles are created through the app wizard (it walks you through column mapping with live previews). Once saved, list them via API:

curl "$API/profiles" -H "$AUTH"

Response:

{
  "profiles": [
    {
      "id": "a27b8f01-...",
      "name": "leads → crm",
      "profileType": "person",
      "isTemplate": false,
      "createdAt": "2026-04-20T12:00:00Z"
    }
  ]
}

Grab the id of the profile that fits your use case.

Step 4 — Create the match job

curl -X POST "$API/jobs" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "sourceFileId": "b1c9f3a0-...",
    "masterFileId": "f4d29b5c-...",
    "profileId":    "a27b8f01-...",
    "matchMode":    "best_match"
  }'

Response (201 Created):

{
  "job": {
    "id": "c93a1f00-...",
    "status": "pending",
    "matchMode": "best_match",
    "createdAt": "2026-04-22T14:32:00Z"
  }
}

matchMode:

  • best_match (default) — each source row pairs with at most one master row (the highest-scoring candidate above threshold).
  • all_candidates — each source row can match multiple master rows; every candidate above threshold is returned.

Use best_match for "dedupe my leads against my CRM" (you want one verdict per lead). Use all_candidates when investigating potential duplicate customers and you want to see every near-miss.

Step 5 — Poll until complete

curl "$API/jobs/c93a1f00-..." -H "$AUTH"

Early response:

{
  "job": {
    "id": "c93a1f00-...",
    "status": "processing",
    "progress": 42,
    "matchMode": "best_match",
    "resultSummary": null,
    "downloadUrl": null,
    "createdAt": "2026-04-22T14:32:00Z",
    "startedAt": "2026-04-22T14:32:03Z",
    "completedAt": null
  }
}

Final response:

{
  "job": {
    "id": "c93a1f00-...",
    "status": "completed",
    "progress": 100,
    "matchMode": "best_match",
    "resultSummary": {
      "total_source_rows": 5000,
      "matched": 3812,
      "review": 244,
      "unmatched": 944
    },
    "downloadUrl": "https://s3...presigned...",
    "createdAt": "2026-04-22T14:32:00Z",
    "startedAt": "2026-04-22T14:32:03Z",
    "completedAt": "2026-04-22T14:33:41Z"
  }
}

Match verdicts (each source row gets exactly one):

  • matched — high confidence (≥ profile threshold, typically 85%).
  • review — mid confidence (20 pts below threshold up to threshold). Human should eyeball these before trusting.
  • unmatched — no candidate above the review floor.

Step 6 — Download the result CSV

The downloadUrl is a presigned S3 URL. It expires in about an hour; if it does, just call GET /jobs/{id} again for a fresh one.

# $RESULT_URL = the downloadUrl from above
curl "$RESULT_URL" -o results.csv

The CSV has three sections of columns, in order:

  1. All original source columns, unchanged.
  2. All master columns from the matched master row (empty when _lmg_match_status == "unmatched").
  3. LMG metadata prefixed _lmg_:
    • _lmg_row_id — stable source row identifier
    • _lmg_match_status — matched / review / unmatched
    • _lmg_match_score — 0-100 confidence score
    • _lmg_match_reason — human-readable reason (e.g. "email + zip")
    • _lmg_master_row_id — id of the matched master row, if any

You should never need to VLOOKUP your own data — the matched row is already stitched in.

Step 7 — Fetch the report

The match job produces a report in parallel: computed rollups, AI narrative, pivots. Find it by jobId:

curl "$API/reports?jobId=c93a1f00-..." -H "$AUTH"

Response:

{
  "reports": [
    {
      "id": "d04b2e91-...",
      "jobId": "c93a1f00-...",
      "status": "completed",
      "progress": 100,
      "createdAt": "2026-04-22T14:33:44Z",
      "completedAt": "2026-04-22T14:34:02Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "returned": 1 }
}

Now pull the full detail:

curl "$API/reports/d04b2e91-..." -H "$AUTH"

Trimmed response:

{
  "report": {
    "id": "d04b2e91-...",
    "status": "completed",
    "rollups": {
      "totals": {
        "sourceRows": 5000,
        "matched": 3812,
        "review": 244,
        "unmatched": 944,
        "matchRatePct": 76.24,
        "avgScore": 87.1,
        "medianScore": 91.0
      },
      "scoreDistribution": [
        { "bucket": "90-100", "count": 3120 },
        { "bucket": "80-89",  "count": 692 },
        { "bucket": "70-79",  "count": 244 }
      ],
      "passBreakdown": [
        { "pass": "email_exact",       "count": 2410, "pct": 48.2 },
        { "pass": "name_zip",          "count": 980,  "pct": 19.6 },
        { "pass": "phone_normalized",  "count": 422,  "pct": 8.4 }
      ],
      "dataQuality": {
        "totalSource": 5000,
        "missingEmail": 140,
        "missingPhone": 823,
        "invalidEmail": 17,
        "duplicateRows": 62
      }
    },
    "narrative": {
      "executive":   "76% of your source file matched your CRM...",
      "dataQuality": "140 rows were missing email...",
      "keyFindings": "The majority of matches came through email..."
    },
    "exports": {
      "pdfReady": false,
      "pptxReady": false,
      "xlsxReady": false
    }
  },
  "job": {
    "sourceFileName": "new_leads_may.csv",
    "masterFileName": "salesforce_master.csv",
    "resultSummary": { "matched": 3812, "review": 244, "unmatched": 944 }
  }
}

Everything is JSON-native — pipe it into a dashboard, Slack digest, weekly report, or your own analytics warehouse.

Step 8 — Export a PDF / XLSX / PPTX

The report object tells you which formats are already generated (exports.pdfReady, etc.). First request triggers generation:

curl "$API/reports/d04b2e91-.../export?format=pdf" -H "$AUTH"

First response (not yet generated):

{
  "status": "generating",
  "message": "Export is being prepared. Poll this endpoint every 10-30 seconds until status='ready'."
}

Poll until:

{
  "status": "ready",
  "downloadUrl": "https://s3...presigned...pdf"
}

Then curl that URL to the file of your choice. Generation takes 10-30 seconds typically.

Tier gates on exports:

FormatRequired tier
xlsxStarter+
pdfPro+
pptxBusiness only

A format above your tier returns 403 with requiredTier in the body so you can surface the upgrade prompt.

Endpoint reference

Every endpoint requires Authorization: Bearer <key>. Every response carries X-RateLimit-* headers.


GET /whoami

Returns the authenticated user and their subscription tier. Use this as a sanity check during integration work.

Request:

curl https://app.listmatchgenie.com/api/v1/whoami \
  -H "Authorization: Bearer $LMG_API_KEY"

Response 200:

{
  "user": { "id": "...", "email": "you@example.com", "name": "You" },
  "tier": "business"
}

GET /files

List the authenticated user's non-deleted files.

Query parameters:

ParamTypeNotes
purposestringsource or master (optional)
limitint1-200, default 50
offsetintdefault 0

Request:

curl "$API/files?purpose=master&limit=10" -H "$AUTH"

Response 200:

{
  "files": [
    {
      "id": "f4d29b5c-...",
      "name": "salesforce_master.csv",
      "purpose": "master",
      "rowCount": 500000,
      "columnCount": 22,
      "fileSizeBytes": 104857600,
      "status": "ready",
      "createdAt": "2026-04-15T09:00:00Z",
      "lastUsedAt": "2026-04-22T14:33:41Z"
    }
  ],
  "pagination": { "limit": 10, "offset": 0, "returned": 1 }
}

POST /files

Two-step upload. First you POST metadata; the response gives you a presigned S3 URL. Then you PUT your raw bytes to that URL. Bytes never pass through our servers.

Request body:

{
  "fileName": "leads.csv",
  "fileSize": 1048576,
  "contentType": "text/csv",
  "purpose": "source"
}

Response 200:

{
  "fileId": "b1c9f3a0-...",
  "uploadUrl": "https://s3...presigned...",
  "s3Key": "users/.../raw.csv"
}

Then upload bytes (client-side, direct to S3):

curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: text/csv" \
  --data-binary @leads.csv

After the PUT, the file auto-enters preparation. Poll GET /files/{fileId} and wait for status: "ready" before matching.

413 File too large — your plan caps per-file size. See docs/data/file-size-and-row-limits for current caps by tier.


GET /files/{fileId}

Single-file metadata including detected columns and cleanse report.

Response 200:

{
  "file": {
    "id": "b1c9f3a0-...",
    "name": "leads.csv",
    "purpose": "source",
    "status": "ready",
    "rowCount": 5000,
    "columnCount": 14,
    "columns": [
      { "name": "email",      "type": "email",  "nullCount": 140 },
      { "name": "first_name", "type": "person", "nullCount": 0 },
      { "name": "zip",        "type": "us_zip", "nullCount": 87 }
    ],
    "cleanseReport": {
      "invalidEmails": 17,
      "normalizedPhones": 4210
    }
  }
}

GET /profiles

List saved match profiles. Profiles are created through the app wizard today. The API surfaces them read-only so you can reference one by ID when creating a job.

Response 200:

{
  "profiles": [
    {
      "id": "a27b8f01-...",
      "name": "leads → crm",
      "profileType": "person",
      "isTemplate": false,
      "createdAt": "2026-04-20T12:00:00Z"
    }
  ]
}

GET /jobs

List match jobs.

Query parameters:

ParamTypeNotes
statusstringpending, processing, completed, failed (optional)
limitint1-200, default 50
offsetintdefault 0

POST /jobs

Create and enqueue a match job. Returns 201 with a pending job; poll GET /jobs/{jobId} for status.

Request body:

{
  "sourceFileId": "b1c9f3a0-...",
  "masterFileId": "f4d29b5c-...",
  "profileId":    "a27b8f01-...",
  "matchMode":    "best_match"
}

matchMode is best_match (default) or all_candidates — see the walkthrough above.

Response 201:

{
  "job": {
    "id": "c93a1f00-...",
    "status": "pending",
    "matchMode": "best_match",
    "createdAt": "2026-04-22T14:32:00Z"
  }
}

Errors:

  • 400 — missing sourceFileId, masterFileId, or profileId.
  • 404 — one of the three IDs doesn't belong to you or was deleted.

GET /jobs/{jobId}

Status, progress, and (once complete) the download URL for the result CSV.

Response 200:

{
  "job": {
    "id": "c93a1f00-...",
    "status": "completed",
    "progress": 100,
    "matchMode": "best_match",
    "resultSummary": {
      "total_source_rows": 50000,
      "matched": 42100,
      "review": 3200,
      "unmatched": 4700
    },
    "downloadUrl": "https://s3...presigned...",
    "createdAt": "...",
    "startedAt": "...",
    "completedAt": "..."
  }
}

downloadUrl is only populated when status === "completed". The URL expires in about an hour — call this endpoint again for a fresh URL if you need it later.


GET /reports

List reports. One report exists per completed match job. Filter by jobId to find the report for a specific match.

Query parameters:

ParamTypeNotes
jobIduuidFilter to a specific match
limitint1-200, default 50
offsetintdefault 0

Response 200:

{
  "reports": [
    {
      "id": "d04b2e91-...",
      "jobId": "c93a1f00-...",
      "title": "Leads → CRM — May",
      "status": "completed",
      "progress": 100,
      "jobLabel": "leads → crm",
      "createdAt": "2026-04-22T14:33:44Z",
      "completedAt": "2026-04-22T14:34:02Z"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "returned": 1 }
}

GET /reports/{reportId}

Full report detail: rollups, narrative sections, export readiness, and joined job context.

Response 200 (abridged — see the walkthrough for the full shape):

{
  "report": {
    "id": "d04b2e91-...",
    "status": "completed",
    "rollups": {
      "totals": {
        "sourceRows": 5000,
        "matched": 3812,
        "review": 244,
        "unmatched": 944,
        "matchRatePct": 76.24
      },
      "scoreDistribution": [ ... ],
      "passBreakdown":     [ ... ],
      "dataQuality":       { ... }
    },
    "narrative": {
      "executive":   "...",
      "dataQuality": "...",
      "keyFindings": "..."
    },
    "exports": { "pdfReady": true, "pptxReady": false, "xlsxReady": true }
  },
  "job": {
    "sourceFileName": "new_leads_may.csv",
    "masterFileName": "salesforce_master.csv",
    "resultSummary": { "matched": 3812, "review": 244, "unmatched": 944 }
  }
}

All fields in rollups are optional — handle missing keys defensively.


GET /reports/{reportId}/export?format=pdf|xlsx|pptx

Fetch a formatted export. Two-phase response:

If already generated:

{
  "status": "ready",
  "downloadUrl": "https://s3...presigned...pdf"
}

If not yet generated (first request enqueues; subsequent polls return the same generating until it's done):

{
  "status": "generating",
  "message": "Export is being prepared. Poll this endpoint every 10-30 seconds until status='ready'."
}

Poll cadence: every 10-30 seconds. Generation is usually done within 30 seconds, faster for small reports.

Tier gates:

FormatTier
xlsxStarter+
pdfPro+
pptxBusiness

A format above your tier returns 403 with requiredTier in the body.

Other errors:

  • 404 — report doesn't exist or isn't yours.
  • 409 — report is not completed yet (still running).

GET /schedules

List recurring match schedules. Read-only today; create / edit goes through the app UI.

Response 200:

{
  "schedules": [
    {
      "id": "s01abc...",
      "name": "Daily lead dedupe",
      "profileId": "a27b8f01-...",
      "sourceFileId": "b1c9f3a0-...",
      "masterFileId": "f4d29b5c-...",
      "intervalType": "daily",
      "hourLocal": 6,
      "timezone": "America/New_York",
      "nextRunAt": "2026-04-23T10:00:00Z",
      "lastRunAt": "2026-04-22T10:00:00Z",
      "lastJobId": "c93a1f00-...",
      "status": "active",
      "createdAt": "2026-04-15T09:00:00Z"
    }
  ]
}

Patterns & recipes

Polling a job efficiently

Back off slightly on longer jobs so you're not burning rate-limit budget polling every second:

import time, requests
 
def wait_for_job(job_id, api, headers, timeout_sec=900):
    deadline = time.time() + timeout_sec
    delay = 2
    while time.time() < deadline:
        r = requests.get(f"{api}/jobs/{job_id}", headers=headers)
        r.raise_for_status()
        job = r.json()["job"]
        if job["status"] in ("completed", "failed"):
            return job
        time.sleep(delay)
        delay = min(delay * 1.5, 15)  # cap at 15s
    raise TimeoutError(f"Job {job_id} didn't complete in {timeout_sec}s")

Respecting rate limits

Every response carries the state. Backoff when X-RateLimit-Remaining is low, rather than catching 429s:

import time
 
def with_rate_limit_backoff(resp):
    remaining = int(resp.headers.get("X-RateLimit-Remaining", "20"))
    reset_ts  = int(resp.headers.get("X-RateLimit-Reset", "0"))
    if remaining <= 2:
        sleep_for = max(0, reset_ts - int(time.time()))
        if sleep_for > 0:
            time.sleep(sleep_for)

On a hard 429 you get Retry-After — sleep exactly that many seconds and retry.

Reusing the master file

Don't re-upload your master file every run. Upload it once, keep the fileId, and reuse it across many source files:

MASTER_FILE_ID = "f4d29b5c-..."   # set once, reuse across runs
 
def run_daily_dedupe(new_leads_path):
    source_id = upload_file(new_leads_path, purpose="source")
    wait_for_file_ready(source_id)
    job = create_job(source_id, MASTER_FILE_ID, PROFILE_ID)
    return wait_for_job(job["id"])

Fetching both CSV and report in one pass

job = wait_for_job(job_id)
 
# Download the raw matches
csv_bytes = requests.get(job["downloadUrl"]).content
with open("results.csv", "wb") as f:
    f.write(csv_bytes)
 
# Find the report for this job
report_list = requests.get(f"{API}/reports?jobId={job_id}", headers=headers).json()
report_id = report_list["reports"][0]["id"]
report = requests.get(f"{API}/reports/{report_id}", headers=headers).json()["report"]
 
# Kick off a PDF export (will poll for ready)
export_url = wait_for_export(report_id, format="pdf")
pdf_bytes = requests.get(export_url).content

Code samples

Python — full pipeline

import os, time, requests
 
API = "https://app.listmatchgenie.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['LMG_API_KEY']}"}
 
# 1. Upload source file
meta = requests.post(f"{API}/files", headers=HEADERS, json={
    "fileName": "leads.csv",
    "fileSize": os.path.getsize("leads.csv"),
    "contentType": "text/csv",
    "purpose": "source",
}).json()
 
with open("leads.csv", "rb") as f:
    requests.put(meta["uploadUrl"],
                 data=f,
                 headers={"Content-Type": "text/csv"}).raise_for_status()
 
source_id = meta["fileId"]
 
# 2. Wait for file to be ready
while True:
    f = requests.get(f"{API}/files/{source_id}", headers=HEADERS).json()["file"]
    if f["status"] == "ready": break
    if f["status"] == "failed": raise RuntimeError(f["prepareError"])
    time.sleep(2)
 
# 3. Fetch master + profile (assume you know these)
MASTER_ID  = os.environ["LMG_MASTER_ID"]
PROFILE_ID = os.environ["LMG_PROFILE_ID"]
 
# 4. Create + poll the match job
job = requests.post(f"{API}/jobs", headers=HEADERS, json={
    "sourceFileId": source_id,
    "masterFileId": MASTER_ID,
    "profileId":    PROFILE_ID,
}).json()["job"]
 
while job["status"] not in ("completed", "failed"):
    time.sleep(3)
    job = requests.get(f"{API}/jobs/{job['id']}", headers=HEADERS).json()["job"]
 
if job["status"] == "failed":
    raise RuntimeError("Match failed")
 
print(f"Matched: {job['resultSummary']['matched']}")
print(f"Result CSV: {job['downloadUrl']}")
 
# 5. Fetch the report
report_list = requests.get(
    f"{API}/reports?jobId={job['id']}", headers=HEADERS,
).json()["reports"]
report = requests.get(
    f"{API}/reports/{report_list[0]['id']}", headers=HEADERS,
).json()["report"]
 
print("Match rate:", report["rollups"]["totals"]["matchRatePct"], "%")
print("Narrative:", report["narrative"]["executive"][:200])
 
# 6. Export a PDF (poll until ready)
while True:
    exp = requests.get(
        f"{API}/reports/{report['id']}/export?format=pdf",
        headers=HEADERS,
    ).json()
    if exp.get("status") == "ready":
        print("PDF:", exp["downloadUrl"])
        break
    time.sleep(15)

JavaScript (Node.js + fetch)

const API = "https://app.listmatchgenie.com/api/v1";
const headers = { Authorization: `Bearer ${process.env.LMG_API_KEY}` };
 
// Upload source
const meta = await fetch(`${API}/files`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    fileName: "leads.csv",
    fileSize: fs.statSync("leads.csv").size,
    contentType: "text/csv",
    purpose: "source",
  }),
}).then((r) => r.json());
 
await fetch(meta.uploadUrl, {
  method: "PUT",
  body: fs.readFileSync("leads.csv"),
  headers: { "Content-Type": "text/csv" },
});
 
// Wait for ready
let file;
do {
  await new Promise((r) => setTimeout(r, 2000));
  file = await fetch(`${API}/files/${meta.fileId}`, { headers })
    .then((r) => r.json()).then((d) => d.file);
} while (file.status !== "ready" && file.status !== "failed");
 
// Create + poll job
const { job: pending } = await fetch(`${API}/jobs`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    sourceFileId: meta.fileId,
    masterFileId: process.env.LMG_MASTER_ID,
    profileId:    process.env.LMG_PROFILE_ID,
  }),
}).then((r) => r.json());
 
let job = pending;
while (job.status !== "completed" && job.status !== "failed") {
  await new Promise((r) => setTimeout(r, 3000));
  job = await fetch(`${API}/jobs/${pending.id}`, { headers })
    .then((r) => r.json()).then((d) => d.job);
}
 
console.log("Result CSV:", job.downloadUrl);
 
// Fetch report
const { reports } = await fetch(
  `${API}/reports?jobId=${job.id}`, { headers },
).then((r) => r.json());
const { report } = await fetch(
  `${API}/reports/${reports[0].id}`, { headers },
).then((r) => r.json());
 
console.log("Match rate:", report.rollups.totals.matchRatePct);

Errors

All errors return a JSON body with an error field:

{ "error": "Invalid or revoked API key" }
CodeWhen
400Missing / malformed request body
401Missing, malformed, or revoked API key
402Feature requires a higher plan (API is Business only)
403Tier limit hit (e.g. export format above your plan)
404Resource not found (wrong ID, deleted, or not owned by you)
409Precondition failed (e.g. report not yet completed)
413File too large for your plan
429Rate limit — retry after Retry-After seconds
500Server error — retry with backoff

Stability

The v1 API is committed. Endpoint paths, request shapes, and response shapes won't break. We may add new endpoints and new fields on existing responses — write your clients defensively so extra fields don't trip them up. Breaking changes would ship as v2 alongside v1 with a deprecation window.

Support

API questions go to support@listmatchgenie.com. Or reach us through the contact form inside the app.