Sign in

API guide

Check one email, send a whole contact list, and get told when it is done. Works with Clay (HTTP API column), Zapier and Make (webhooks and HTTP steps), n8n (HTTP Request node) or any code.

Your API key

Make a key on Account, API keys. It starts with lh_live_ and is shown once. Send it with every call:

Authorization: Bearer lh_live_your_key_here

Keep it secret. If it leaks, turn it off on the same page and make a new one.

Check one email

Runs the free checks (spelling, throwaway domains, shared inboxes, the company domain takes email, the website is up). If those pass, it also asks the mail server whether the mailbox exists. That mailbox check counts toward your plan's monthly email checks. Send "mailbox": false for the free checks only.

curl -X POST https://listhealth-app.vercel.app/api/v1/verify \
  -H "Authorization: Bearer $LISTHEALTH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "ana.lopez@example.com"}'

Answer:

{
  "email": "ana.lopez@example.com",
  "verdict": "ok",              // ok, warning or problem
  "status": "healthy",          // healthy, dead_email, catch_all, unknown, passed_free or problem
  "reason": "Mailbox accepts email",
  "detail": "Email by Google Workspace · Website: up",
  "mailbox_checked": true,
  "email_result": "valid",      // valid, invalid, catch_all, unknown, disposable (null if not checked)
  "note": null                  // why the mailbox was not checked, when it was not
}

Send a list

Send your contacts as JSON rows, up to 5,000 per call. Name the email field email (or a website field website for a company list); we recognise the usual names for first name, last name, company, title and LinkedIn. Any other fields are kept and come back in the download. The list is checked in the background, exactly like an upload in the app, and counts as one of your lists.

curl -X POST https://listhealth-app.vercel.app/api/v1/audits \
  -H "Authorization: Bearer $LISTHEALTH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "October outbound list",
    "rows": [
      {"email": "ana.lopez@example.com", "first_name": "Ana", "last_name": "Lopez", "company": "Example Co", "title": "VP Sales"},
      {"email": "sam.okafor@example.org", "first_name": "Sam", "last_name": "Okafor", "company": "Sample Ltd", "title": "Head of RevOps"}
    ]
  }'

Answer (202):

{
  "id": "6f1c9a3e-0000-4000-8000-000000000000",
  "status": "queued",
  "rows": 2,
  "notes": [],
  "status_url": "https://listhealth-app.vercel.app/api/v1/audits/6f1c9a3e-0000-4000-8000-000000000000",
  "report_url": "https://listhealth-app.vercel.app/audits/6f1c9a3e-0000-4000-8000-000000000000"
}

If your field names are unusual, say which is which: "columns": {"email": "Work Email", "company": "Account Name"}.

Get the results

curl https://listhealth-app.vercel.app/api/v1/audits/<id> -H "Authorization: Bearer $LISTHEALTH_KEY"

Gives the progress: status is queued, checking, done or failed, with the health score and counts. When it is done, add ?include=rows for every contact, 1,000 at a time (use next_offset for the next page: ?include=rows&offset=1000).

{
  "id": "6f1c9a3e-...", "name": "October outbound list", "status": "done", "finished": true,
  "rows": 2, "health_score": 50,
  "counts": {"healthy": 1, "left_company": 1},
  "leavers": {"checked": 1, "left": 1, "replacements": 1},
  "results": [
    {"row": 0, "email": "ana.lopez@example.com", "name": "Ana Lopez", "status": "healthy",
     "action": "keep", "left_company": false, "replacement": null},
    {"row": 1, "email": "sam.okafor@example.org", "name": "Sam Okafor", "status": "left_company",
     "action": "update", "left_company": true, "current_company": "Another Example Inc",
     "replacement": {"name": "Lee Park", "title": "Head of RevOps", "linkedin": null, "email": null}}
  ],
  "next_offset": null
}

action is what to do in your CRM: keep, remove, update or review.

Webhook when a list is done

Save an https address on Account, API keys (for example a Zapier "Catch Hook" or an n8n Webhook node). When any of your lists finishes, we POST this to it, usually within a minute or two:

{
  "event": "audit.finished",
  "audit": { "id": "6f1c9a3e-...", "name": "October outbound list", "status": "done", "health_score": 50, "counts": {...}, ... },
  "results_url": "https://listhealth-app.vercel.app/api/v1/audits/6f1c9a3e-...?include=rows"
}

The contacts are not in the webhook; fetch results_url with your key. If your address does not answer with a 2xx code we try again after 1, 5 and 30 minutes, then stop.

Optional check that a call is really from us: every call has a header X-ListHealth-Signature: t=<time>,v1=<signature>. The signature is an HMAC-SHA256 of <time>.<raw body> with the signing secret shown when you saved the address. In Node:

const [t, v1] = header.match(/t=(\d+),v1=([a-f0-9]+)/).slice(1);
const expected = crypto.createHmac("sha256", SIGNING_SECRET).update(t + "." + rawBody).digest("hex");
const real = expected === v1 && Math.abs(Date.now() / 1000 - Number(t)) < 300;

Limits and errors

Errors come back as {"error": "what went wrong, in plain words"} with:

Questions or a tool we should support? Email hello@example.com.