ListCheckup Email Verification

Verify email addresses and whole lists from Claude, Cursor or any MCP client. Price a list before spending, catch-all and unknown results are free.

Hosted MCP Server

npx add-mcp 'https://listcheckup.com/mcp'

Installs into Claude Code, Codex, Cursor and more

Documentation

API

Check one address, or a list of any size, from your own code. Same credits as the dashboard, same verdicts.

A REST API over HTTPS. Everything is JSON, every field is snake_case, and the base is https://listcheckup.com. There is no SDK to install and nothing to wait for — create a key and the two commands below work.

Authentication

Every request carries an API key, created in your dashboard. Keys start lck_live_, can be given an expiry, and can be revoked at any time.

curl "https://listcheckup.com/v1/me" \
  -H "Authorization: Bearer lck_live_..."

GET /v1/me answers with the account the key belongs to and its balance, which makes it the quickest way to confirm a key works:

{ "email": "[email protected]", "credits": 24600 }

Without a key, or with one that has been revoked or has expired, every endpoint answers 401 and says which of those it was — a revoked key and a typo call for different fixes.

Rate limit

120 requests to begin with, refilling at 60 a minute, per key. That is a ceiling on request volume rather than on addresses: a list of half a million addresses is one request. Exceeding it answers 429, and waiting is the fix.

Verify one address

Answers in the response. Use it when somebody is waiting — a signup form, a checkout — rather than for a list you already hold.

curl "https://listcheckup.com/v1/[email protected]" \
  -H "Authorization: Bearer lck_live_..."

There is a POST /v1/verify with { "email": "..." } in the body if a query string is awkward. Both return the same thing:

{
  "email": "[email protected]",
  "normalized_email": "[email protected]",
  "local_part": "someone",
  "domain": "example.com",
  "status": "valid",
  "reason": "mailbox_accepted",
  "confidence": 95,
  "duration_ms": 412
}

status is one of five values, and confidence is 0–100 for how much that status can be relied on. A catch-all scores low even though the server accepted the address, because acceptance proves nothing there.

statusWhat it means
validThe mailbox exists and can receive mail.
invalidIt cannot receive mail. Remove it.
riskyIt works, but it is a throwaway address or a shared inbox rather than a person.
catch_allThe domain accepts every address, so nobody can tell whether this one exists.
unknownThe mail server would not answer. A fact about their policy, not the address.

You are only charged for an answer. catch_all and unknown cost nothing, because the work was done and the mail server declined to say — billing for those would be billing you for our inability to find out.

Verify a list

Four calls: create, start, check, collect.

1. Create

Addresses as JSON, not a file upload — you already hold them as strings, and building a multipart body to send strings is work for nothing.

curl -X POST "https://listcheckup.com/v1/lists" \
  -H "Authorization: Bearer lck_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name": "march-subscribers", "emails": ["[email protected]", "[email protected]"]}'

Nothing is charged yet. The response is a list summary with the price, so you can decide before spending anything:

{
  "id": "7f3c1d20-9a4b-4c8e-8f21-5b6d7e8a9c04",
  "name": "march-subscribers",
  "status": "awaiting_confirmation",
  "addresses": 24600,
  "duplicates": 1020,
  "not_addresses": 180,
  "unique": 23580,
  "excluded": 0,
  "price": 23400,
  "checked": 0,
  "credits_charged": 0,
  "failure_reason": null,
  "created_at": "2026-03-18T09:12:00+00:00",
  "completed_at": null
}

price is in credits and is what starting it will cost. Repeats are counted once and lines that are not addresses are never charged, which is why it is lower than addresses.

2. Start

curl -X POST "https://listcheckup.com/v1/lists/<id>/start" \
  -H "Authorization: Bearer lck_live_..."

This is the call that spends credits. 402 means the balance is short, and nothing was charged or started.

3. Know when it is done

Register a webhook and we POST to your server the moment a list finishes — signed, retried if your end is down. That is the way to do this.

If you would rather ask, GET /v1/lists/<id> returns the same summary as above with status and checked moving. Poll it sparingly; a webhook costs you no requests at all.

statusMeaning
uploadedAccepted, not yet read.
readingBeing counted. price is null until this finishes.
awaiting_confirmationCounted and priced. Nothing is charged until you start it.
queuedStarted and waiting for a worker.
runningIn progress. checked climbs.
completedFinished. Results are ready.
failedStopped. See failure_reason.
cancelledStopped by you. Unused credits are returned.

4. Collect the results

curl "https://listcheckup.com/v1/lists/<id>/results?limit=500&skip=0" \
  -H "Authorization: Bearer lck_live_..."

limit defaults to 500 and is capped at 2,000; page with skip. Add include=valid,risky to narrow it to the categories you want.

{
  "id": "7f3c1d20-...",
  "status": "completed",
  "total": 24600,
  "offset": 0,
  "returned": 500,
  "results": [
    {
      "row": 1,
      "email": "[email protected]",
      "status": "valid",
      "confidence": 95,
      "reason": "mailbox_accepted",
      "billable": true,
      "duplicate_of_row": null
    }
  ]
}

row is the position in the list you submitted, so results line up with your own records without matching on the address.

Delete

DELETE /v1/lists/<id> removes a list and its results. Anything unspent on a list that never ran comes back.

A key and two commands — there is nothing to install and nothing to wait for.

Webhooks

Add an endpoint in the dashboard and we send a signed POST when a list finishes:

{
  "event": "list.finished",
  "occurred_at": "2026-03-18T09:41:00+00:00",
  "list": {
    "id": "7f3c1d20-...",
    "name": "march-subscribers",
    "checked": 24600,
    "safe_to_send": 12000,
    "to_remove": 7400,
    "uncertain": 3100
  }
}

Answer with any 2xx. Anything else and we try again, six times over about a day. Each request carries an HMAC-SHA256 signature you check with your signing secret; the exact scheme, with a worked example, is shown beside the secret when you create the endpoint.

Slack works without any of that. Paste a Slack incoming webhook URL instead of your own, and we send a message Slack can display rather than the JSON above — nothing to write, nothing to host. Retries and the delivery log work the same way.

Use it from an AI assistant

There is an MCP server at https://listcheckup.com/mcp. Point an assistant that supports MCP — Claude, or an editor like Cursor — at that URL with the same API key, and it can check addresses for you without anyone writing code.

Connecting it

However you connect, the key travels the same way: an Authorization: Bearer <key> header. Only where you write it differs.

Claude Code — one command:

claude mcp add --transport http listcheckup https://listcheckup.com/mcp \
  --header "Authorization: Bearer lck_live_..."

Claude Desktop, Cursor and most other clients — in the config file:

{
  "mcpServers": {
    "listcheckup": {
      "url": "https://listcheckup.com/mcp",
      "headers": { "Authorization": "Bearer lck_live_..." }
    }
  }
}

Checking it works

Before wiring it into anything, ask the server what it can do:

curl -X POST "https://listcheckup.com/mcp"   -H "Authorization: Bearer lck_live_..."   -H "Content-Type: application/json"   -H "Accept: application/json, text/event-stream"   -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Six tools listed means you are connected. "error": "unauthorized" means the key did not arrive — nearly always the word Bearer and its space missing, or the key clipped short when it was pasted. A key is about fifty characters and starts lck_live_.

What it can do

ToolWhat it does
check_emailOne address. Costs a credit unless the answer is catch-all or unknown.
get_balanceCredits left. Free.
create_listSubmit a list and get a price. Charges nothing.
start_listBegin checking. This is the call that spends credits.
get_listStatus and progress. Free.
get_list_summaryThe counts once it has finished. Free.

Pricing and starting are two separate tools deliberately, so an assistant has to show you what a list will cost before anything is spent, and you get to say no. No tool takes an account as an argument — the key in the header decides whose credits are used, so nothing an assistant reads or is told can point it at somebody else’s account.

Treat the key as a password

Anything holding it can spend your credits. It sits in a configuration file in clear text, so keep that file out of a shared repository, and revoke the key if it goes somewhere it should not have — revoking takes effect immediately, for the API and this server alike. Give each machine its own key and you can revoke one without disturbing the rest.

Errors

Every error is JSON with a stable code and a sentence meant for a human:

{ "error": "unauthorized", "message": "Provide your API key as 'Authorization: Bearer <key>'." }
CodeMeans
400Something in the request is missing or wrong. The body says which.
401No key, or one that is unknown, revoked or expired.
402Not enough credits. Nothing was charged or started.
404No such list on this account.
409The list is not in a state where that makes sense — already started, say.
429Rate limited. Wait.

What it will not do

The API checks addresses you already hold. It does not find, generate or supply addresses, and it never sends mail to the addresses it checks — the conversation with each mail server is closed before a message exists. Using it on scraped, purchased or rented lists is prohibited; see acceptable use.

Getting started

Create an account, make a key in the dashboard, and run the /v1/me command above. ListCheckup gives you free credits at sign-up, so the first checks cost nothing.

Related: what credits cost, and how to choose a verification service.