API Documentation

Base URL: https://mailprobe.dev

All API requests require authentication via your API key in the Authorization header.

Developer Resources

OpenAPI 3.0 specification — generate a client in any language, import it into Swagger, Insomnia or Bruno, or hand it to an agent. It documents every field, enum and error of the two endpoints below.

Postman collection — import it, paste your API key into the api_key variable, and verify an address in under a minute.

Use MailProbe with n8n

The official MailProbe node for n8n verifies the addresses that flow through your workflows. On a self-hosted n8n, open Settings > Community Nodes, select Install and enter n8n-nodes-mailprobe, then create a MailProbe API credential with your key.

The node sends your items 20 at a time, waits out the rate limit on its own and returns one result per item, in the input order. It also works as a tool for the n8n AI Agent. Source, usage notes and changelog are on GitHub.

Use MailProbe with Zapier

The official MailProbe app on Zapier adds a Verify Email action to your Zaps. Add it as an action step, connect it with your API key, and map the address to check: the step returns the same verdict as the API, one address per run. Zapier gives a step 30 seconds: when a slow mail server holds the check longer, the step stops with an explanation and you can replay it later. Zapier also lets AI assistants such as Claude, ChatGPT and Cursor call Verify Email through Zapier MCP. The source is on GitHub.

Use MailProbe with AI assistants (MCP)

MailProbe runs a Model Context Protocol server at https://mailprobe.dev/mcp. Add it to an MCP client such as Claude Code, Cursor or VS Code and the assistant verifies addresses on its own: verify_emails takes 1 to 20 addresses and returns the same results as the API, and get_credits returns your balance. The server speaks Streamable HTTP and identifies you with your API key, sent in the Authorization header. Calls made this way use your credits and your rate limit exactly like API calls.

A call to this server answers within about 55 seconds, because MCP clients give up on a tool call after a minute. An address that has no verdict yet at that point comes back unknown with reason timeout, at no charge: the assistant can send it again.

In Claude Code:

claude mcp add --transport http mailprobe https://mailprobe.dev/mcp \
  --header "Authorization: Bearer mp_live_abc123..."

In Cursor, in .cursor/mcp.json:

{
  "mcpServers": {
    "mailprobe": {
      "url": "https://mailprobe.dev/mcp",
      "headers": { "Authorization": "Bearer mp_live_abc123..." }
    }
  }
}

Any other client that connects to a remote MCP server and can send a header works the same way.

For a client that starts its servers on your computer, such as Claude Desktop, the official mailprobe-mcp package runs the same two tools locally. It needs Node.js 22 or later, and reads your API key from the environment variable set below:

{
  "mcpServers": {
    "mailprobe": {
      "command": "npx",
      "args": ["-y", "mailprobe-mcp"],
      "env": { "MAILPROBE_API_KEY": "mp_live_abc123..." }
    }
  }
}

In Claude Code, the MailProbe plugin connects to the remote server and adds a skill that tells the assistant how to read a result, what a call costs and how to go through a list:

/plugin marketplace add jamalofski/mailprobe-mcp
/plugin install mailprobe@mailprobe

The Claude and ChatGPT apps connect to a remote server only through an OAuth sign-in, which this server does not offer yet. Claude Desktop takes the local server above; elsewhere, MailProbe is available through Zapier MCP. Source, plugin and skill are on GitHub.

Authentication

Include your API key in every request:

Authorization: Bearer mp_live_your_api_key_here

Create a key under Developer in your account. You can hold several — one per environment or integration — and they all draw on the same credits. A key is shown once, at creation: we only store its hash, so it cannot be displayed again. Deleting a key stops it working immediately.

Verify Emails

POST /api/v1/verify

Send 1 to 20 emails per request. For longer lists, the batch screen in your account takes a pasted list or a CSV file and runs it in the background.  Each email costs 1 credit, and only what we actually probe is charged: a duplicate inside the same request is verified once and charged once, a syntactically invalid entry costs nothing, and a credit is returned when the receiving server refuses our connection before evaluating the address (unknown with reason policy_rejected, no_banner, ehlo_rejected or mail_from_rejected). Refunds land on your balance automatically.

A request answers within about 95 seconds, however slow or silent the mail servers are. An address that has no verdict yet at that point comes back unknown with reason timeout, at no charge. Send it again later.

To get the answer sooner, add timeout to the body: a whole number of seconds, from 5 to 95, as in {"emails": ["verify@mailprobe.dev"], "timeout": 25}. Use it when something in front of your code gives up before 95 seconds, such as a gateway, a sign-up form or an automation step with a time limit. The same rule applies at that point: an address that has no verdict yet comes back unknown with reason timeout, at no charge.

Example: Single email

curl -X POST \
  -H "Authorization: Bearer mp_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"emails": ["zzq-no-such-mailbox-4821@gmail.com"]}' \
  "https://mailprobe.dev/api/v1/verify"

Example: Multiple emails

curl -X POST \
  -H "Authorization: Bearer mp_live_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"emails": ["verify@mailprobe.dev", "zzq-no-such-mailbox-4821@gmail.com"]}' \
  "https://mailprobe.dev/api/v1/verify"

Example: Node.js

Node.js 18 or later, no dependency. Like the two examples after it, it sends the same request and stops on an error response, whose body is an object, not an array.

async function verify(emails) {
  const res = await fetch("https://mailprobe.dev/api/v1/verify", {
    method: "POST",
    headers: {
      "Authorization": "Bearer mp_live_abc123...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ emails }),
  });
  if (!res.ok) {
    // An error body is an object, not an array
    throw new Error(`${res.status} ${await res.text()}`);
  }
  return res.json();
}

verify(["verify@mailprobe.dev", "zzq-no-such-mailbox-4821@gmail.com"]).then((results) => {
  for (const r of results) console.log(r.email, r.status, r.score);
});

Example: Python

Standard library only.

import json
import urllib.error
import urllib.request


def verify(emails):
    request = urllib.request.Request(
        "https://mailprobe.dev/api/v1/verify",
        data=json.dumps({"emails": emails}).encode(),
        headers={
            "Authorization": "Bearer mp_live_abc123...",
            "Content-Type": "application/json",
        },
    )
    try:
        with urllib.request.urlopen(request) as response:
            return json.load(response)
    except urllib.error.HTTPError as error:
        # An error body is an object, not an array
        raise RuntimeError(f"{error.code} {error.read().decode()}") from None


for r in verify(["verify@mailprobe.dev", "zzq-no-such-mailbox-4821@gmail.com"]):
    print(r["email"], r["status"], r["score"])

Example: PHP

With the cURL extension.

<?php

function verify(array $emails): array
{
    $curl = curl_init("https://mailprobe.dev/api/v1/verify");
    curl_setopt_array($curl, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            "Authorization: Bearer mp_live_abc123...",
            "Content-Type: application/json",
        ],
        CURLOPT_POSTFIELDS => json_encode(["emails" => $emails]),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $body = curl_exec($curl);
    if ($body === false) {
        throw new RuntimeException(curl_error($curl));
    }
    $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    if ($status !== 200) {
        // An error body is an object, not an array
        throw new RuntimeException($status . " " . $body);
    }
    return json_decode($body, true);
}

foreach (verify(["verify@mailprobe.dev", "zzq-no-such-mailbox-4821@gmail.com"]) as $r) {
    echo $r["email"], " ", $r["status"], " ", $r["score"], PHP_EOL;
}

Response

Always returns an array, even for a single email. This is a real response, reproducible with the free tester on the homepage.

[
  {
    "email": "zzq-no-such-mailbox-4821@gmail.com",
    "result": "undeliverable",
    "status": "invalid",
    "score": 0,
    "catch_all": false,
    "reason": null,
    "retry_after": null,
    "smtp_code": 550,
    "smtp_enhanced_code": "5.1.1",
    "syntax": true,
    "mx_found": true,
    "smtp_check": true,
    "disposable": false,
    "role_based": false,
    "free_provider": true,
    "did_you_mean": null,
    "mx_records": [
      "gmail-smtp-in.l.google.com",
      "alt1.gmail-smtp-in.l.google.com",
      "alt2.gmail-smtp-in.l.google.com",
      "alt3.gmail-smtp-in.l.google.com",
      "alt4.gmail-smtp-in.l.google.com"
    ]
  }
]

result is the raw SMTP verdict; status is the actionable rollup you filter on; score is a 0–100 confidence value. reason explains a non-deliverable / unknown verdict (e.g. no_mx, accept_all, greylisting, mailbox_full, provider_blocks_probe, mx_unresolved, policy_rejected), and is null otherwise. mailbox_full means the server confirmed the mailbox but it is over quota (4.2.2): it exists, nothing can be delivered today — status is risky. mx_unresolved is transient when result is unknown (our resolver got no answer) and permanent when it is undeliverable (DNS says the MX host does not exist or has no address). retry_after is the suggested wait in seconds whenever the server answered with a temporary (4xx) deferral — greylisting, a busy server, a temporary sender block — else null. catch_all is true when the domain accepts every address. mx_records lists the domain’s MX hosts in priority order, capped at the first 10. smtp_check reports that an SMTP conversation actually took place, not that the mailbox was accepted — it is true above even though the server answered 550; the verdict itself is in result and status. smtp_enhanced_code is the RFC 3463 status the server sent next to that reply code, or null if it sent none — it is what tells a bare 550 apart: 5.1.1 above says the mailbox does not exist, whereas 5.7.x would say the server refused our probe (our IP, HELO or MAIL FROM) and told you nothing about the address. did_you_mean suggests a likely typo fix (e.g. gmial.com → gmail.com) or null. Entries that aren't strings are returned as {"email": <value>, "error": "invalid"} with no result field.

Result Values

The raw technical verdict from the verification pipeline.

ValueDescription
deliverableSMTP confirmed the mailbox exists
undeliverableSMTP rejected the address
catch-allServer accepts all addresses (can't confirm individually)
unknownCould not determine (timeout, connection refused, greylisting, sender rejected by policy)

Status Values

The actionable classification most integrations filter on.

ValueDescription
validDeliverable and clean — safe to send
invalidBad syntax, no MX, or the mailbox rejects — do not send
riskyAccepted but uncertain: catch-all, role-based, disposable, or a provider that blocks probing (Microsoft consumer domains — outlook.com, hotmail.com, live.com, msn.com)
unknownCould not be determined by our probe: either transient (greylisting, deferred, timeout, connection refused, unresolved MX — worth a retry) or a permanent refusal of our probe (policy_rejected, no_banner, ehlo_rejected, mail_from_rejected) that will recur on retry

Score

A deterministic 0–100 confidence value derived only from the signals already in the response — no black-box model, no stored data. Rough bands: 90+ deliverable and clean; ~65 probe-blocking provider (well-formed address on a live Microsoft-consumer MX); ~55 catch-all; ~50 greylisted at RCPT TO (the server evaluated the recipient before deferring); ~30 undetermined — timeout, unresolved MX, or a deferral received before we could ask about the mailbox (deferred); ≤15 disposable; 0 invalid. Role-based and suspected-typo addresses are penalised. Threshold it to fit your risk tolerance per campaign.

Check Credit Balance

GET /api/v1/credits

Example

curl -H "Authorization: Bearer mp_live_abc123..." \
  "https://mailprobe.dev/api/v1/credits"

Response

{ "credits": 847 }

Credits are valid for 12 months from your most recent purchase; an expired balance returns 0.

Error Codes

HTTPCodeDescription
400INVALID_INPUTMissing or empty emails array, or a timeout that is not a whole number from 5 to 95
400TOO_MANYBulk request exceeds 20 emails
401UNAUTHORIZEDMissing or invalid API key
402NO_CREDITSNo credits remaining, buy more
402PAID_ONLY_DOMAINThe request includes a domain that free credits don't cover (the message names it) — nothing is charged, buy credits to verify it
413—JSON body above 50 kB — split the batch (no code field on this one)
429RATE_LIMITEDToo many requests — retry after the number of seconds given in the Retry-After header
500INTERNAL_ERRORServer error

Rate Limits

API requests are limited by your credit balance (1 credit per verification) and capped at 60 requests per minute per API key. A second cap counts every request coming from the same IP address, all keys combined: 300 requests per minute. Exceeding either limit returns 429 RATE_LIMITED with a Retry-After header: the number of seconds to wait before sending the request again. Bulk requests accept up to 20 emails each.

Freshness

Every verification is performed in real time, no caching. Each request produces a fresh result and costs 1 credit.