MailQuell email verification API
Check whether an email address exists and can receive mail, without sending any email: one address in real time with POST /v1/verify, or up to 50,000 addresses at once with POST /v1/bulk. 1 credit per address; addresses that get no clear answer from their mail server, and badly formed ones, are free. 200 free checks every month, no card needed.
https://api.mailquell.comQuick start
- and confirm your email address (200 checks a month are included).
- In Developer Hub → API keys, create a key. It starts with
mq_live_and is shown once: keep it on your server, never in a web page or a mobile app. - Check an address:
curl -X POST https://api.mailquell.com/v1/verify \
-H "X-API-Key: $MAILQUELL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "alex@example.com"}'Authentication
Send your key with every request, in either header:
X-API-Key: mq_live_YOUR_API_KEY # or Authorization: Bearer mq_live_YOUR_API_KEY
- Permissions. A read-only key can read lists, results and credits, but cannot check addresses or start, pause or cancel lists (
403 api_key_read_only). Use one for dashboards and reports. Other keys can do everything the API offers. - Keys belong to the account: a team's keys spend the team's credits. Revoke a key at any time in the Developer Hub; it stops working at once.
- Spending credits needs a confirmed email address (
403 email_not_verified).
Verify one address
https://api.mailquell.com/v1/verifyRead & write keyChecks the syntax, the domain, its mail servers, and asks the mail server whether the mailbox exists (no email is sent). Most answers take a few seconds; allow up to 60 seconds for slow mail servers.
Request body
| Field | Type | Description |
|---|---|---|
| string | The address to check (required). | |
| creditType | "daily" | "instant" | "auto" | Which credits to use. daily (default): your free or plan allowance. instant: purchased credits. auto: the allowance first, then purchased credits. |
| mode | "full" | "quick" | full (default) also tests whether the domain accepts every address (catch-all). quick skips that test: faster, for checks while someone fills in a form. |
Response
{
"email": "alex@example.com",
"status": "safe",
"riskLevel": "low",
"suggestedAction": "safe_to_contact",
"score": 100,
"scoreBreakdown": {
"syntaxScore": 15, "dnsDomainScore": 25, "mxRecordsScore": 25,
"smtpMailboxScore": 35, "penalties": 0,
"penaltyReasons": [], "totalScore": 100
},
"technicalDetails": {
"mailboxExists": true, "domainValid": true, "mxRecordsFound": true,
"mxHost": "mx1.example.com", "smtpReachable": true,
"smtpCode": 250, "smtpMessage": "2.1.5 OK",
"disposable": false, "roleAccount": false, "catchAll": false,
"freeProvider": false, "spfValid": true, "dmarcValid": true,
"riskSignals": []
},
"didYouMean": null,
"durationMs": 2140,
"verifiedAt": "2026-10-30T09:12:44.512Z"
}| Field | Description |
|---|---|
| status | The verdict: see Statuses. |
| suggestedAction | safe_to_contact, send_with_caution or do_not_send. The simplest field to act on. |
| score | 0 to 100. 85 and above is low risk, 50 to 84 medium, below 50 high. |
| riskLevel | low, medium or high, from the score (invalid and disabled are always high). |
| didYouMean | The address with a misspelled domain corrected (gmial.com → gmail.com), or null. |
| technicalDetails.riskSignals | Spam-trap and abuse signals: new_domain, parked_domain, blocklisted_domain, random_local_part, typo_domain. An address with a signal is never safe. |
| technicalDetails.* | What each step found: MX host, SMTP code and message, disposable, role, catch-all, free provider, SPF and DMARC. |
| scoreBreakdown | How the score was built: points per step and the reasons for penalties. |
Cost. 1 credit. Not charged: a badly formed address (answered at once as invalid), an unknown, timeout or blocked answer, and a check that fails on our side (502 verification_failed).
Statuses
| status | Meaning | Charged |
|---|---|---|
| safe | The mail server confirmed the mailbox exists. | Yes |
| invalid | The domain does not exist, has no mail server, or the server said the mailbox does not exist. | Yes (free if badly formed) |
| disposable | A temporary inbox service; these addresses expire within hours. | Yes |
| disabled | The mailbox exists but is disabled or suspended. | Yes |
| inbox_full | The mailbox exists but is over its storage quota. | Yes |
| catch-all | The domain accepts every address, so this mailbox cannot be confirmed. | Yes |
| role | A shared address such as info@ or sales@. Deliverable, but more complaints and lower engagement. | Yes |
| risky | Deliverable, but other signals (a risk signal, a weak domain setup) lower the score. | Yes |
| unknown | The mail server gave no clear answer (greylisting, temporary error). Try again later. | No |
| timeout | The mail server did not answer in time. | No |
| blocked | The mail server refused to answer our check. | No |
Verify a list
https://api.mailquell.com/v1/bulkRead & write keyStarts a list in the background and answers at once with 201. Up to 50,000 addresses per list and 25 MB per request; 10 lists a minute.
| Field | Type | Description |
|---|---|---|
| name | string | Shown in your dashboard (required, up to 200 characters). |
| emails | array | Rows of { email, name?, company? } (required). Name and company come back with the results. |
| creditType | "instant" | "daily" | Which credits to use. Default instant (purchased credits). |
| removeDuplicates | boolean | Default true: the same address is checked and charged once. false: every row is checked and charged. |
| reuseRecent | boolean | Default true: an address your account checked in the last 20 days gets that result again, for free. false: check everything again. |
The response holds task (with its id), duplicatesRemoved and reused. The credits for the whole list are taken when it starts. When it ends, addresses still without a clear answer after a second try (a few minutes later) and checks that failed on our side are refunded.
curl -X POST https://api.mailquell.com/v1/bulk \
-H "X-API-Key: $MAILQUELL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter October",
"emails": [
{ "email": "ann@acme.com", "name": "Ann", "company": "Acme" },
{ "email": "bob@example.org" }
]
}'Follow and read a list
https://api.mailquell.com/api/tasks/:idstatus (processing, completed, paused, cancelled, failed), total, processed, progress (percent), the counters validCount, riskyCount, invalidCount, unknownCount, errorsCount, and resultsExpireAt.
https://api.mailquell.com/api/tasks/:id/results?page=1&pageSize=500&status=safeOne page of results (pageSize up to 500, default 50), in a stable order, optionally for one status. Answer: { items, page, pageSize, total }. Each item has email, name, company, status, riskLevel, score, suggestedAction, reason, scoreBreakdown and technicalDetails.
https://api.mailquell.com/api/tasks/:id/export?format=json&status=safe,catch-allEvery result in one file: format=csv (default) or json, optionally only some statuses (comma-separated).
https://api.mailquell.com/api/tasks/:id/breakdownHow many results per status, and how many addresses have a risk signal.
https://api.mailquell.com/api/tasks?status=completed&page=1&pageSize=20Your lists, newest first (pageSize up to 100).
Pause, resume, cancel
https://api.mailquell.com/api/tasks/:id/pauseRead & write keyhttps://api.mailquell.com/api/tasks/:id/resumeRead & write keyhttps://api.mailquell.com/api/tasks/:id/cancelRead & write keyA paused list keeps its place. A cancelled list refunds the addresses it had not checked. These answer 409 when the list is not in the right state (not_processing, not_paused, still_pausing, not_cancellable).
Retention. Per-address results are deleted 20 days after a list finishes; the counters stay. After that, results and exports answer 410 results_expired. Download what you need before.
Account and credits
https://api.mailquell.com/v1/accountWho the key belongs to and what it can spend. Integrations call it to test a key.
{
"email": "you@company.com",
"name": "Your name",
"plan": "Free",
"credits": { "allowanceLeft": 180, "allowancePeriod": "month", "instant": 5000 }
}allowanceLeft is what remains of the free or plan allowance (renewed every day or month), instant the purchased credits, which never expire. See pricing.
Webhooks
Instead of polling, receive an HTTPS POST when something happens. Add an endpoint in Developer Hub → Webhooks and choose its events; each endpoint has its own signing secret (whsec_…).
| Event | When | data |
|---|---|---|
| task.completed | A list has finished. | id, name, total, processed, counts, completedAt, resultsPath |
| verification.completed | A single check (POST /v1/verify) has finished. | The same result as the API response. |
| credits.low | A credit balance has just gone below the alert threshold. | pool (daily or instant), remaining, threshold |
| webhook.test | You pressed "Send test" in the Developer Hub. | message |
POST /your/endpoint
Content-Type: application/json
MailQuell-Event: task.completed
MailQuell-Delivery: 9f0c… (the same on every retry: use it to ignore duplicates)
MailQuell-Signature: t=1761815564,v1=5257a869e7ec…
{
"id": "evt_3b1f…",
"type": "task.completed",
"created": "2026-10-30T09:12:44.512Z",
"data": {
"id": "cmh…", "name": "Newsletter October", "total": 2000, "processed": 2000,
"counts": { "valid": 1712, "risky": 140, "invalid": 121, "unknown": 27, "errors": 0 },
"completedAt": "2026-10-30T09:12:44.000Z",
"resultsPath": "/api/tasks/cmh…/export"
}
}Check the signature
v1 is the HMAC-SHA256 of "<t>.<raw body>" with your secret, in hex. Refuse a request whose signature does not match or whose timestamp is more than 5 minutes old.
import crypto from "node:crypto";
// Use the raw request body (before JSON parsing), e.g. express.raw({ type: "application/json" }).
function verifyMailQuell(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=", 2)));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// verifyMailQuell(req.body.toString("utf8"), req.get("MailQuell-Signature"), process.env.MAILQUELL_WEBHOOK_SECRET)Delivery
- Answer with any 2xx status within 10 seconds. Redirects are not followed.
- Otherwise we try again after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours (6 attempts over about 7.5 hours), then you are notified in the dashboard. Every delivery is listed in the Developer Hub, where you can send it again.
- The endpoint must use HTTPS on the standard port and a public address.
Rate limits
120 requests per minute per API key (without a key: per IP address), and 10 list uploads per minute. Every response includes:
x-ratelimit-limit: 120 x-ratelimit-remaining: 117 x-ratelimit-reset: 42
Over the limit you get 429 rate_limited with a retry-after header (seconds). For more than a few addresses, send one list with POST /v1/bulk instead of many single checks.
Errors
Errors come back as JSON with an error code, sometimes with details: { "error": "insufficient_credits", "pool": "instant", "needed": 2000 }.
| HTTP | error | What to do |
|---|---|---|
| 400 | invalid_body, invalid_json, invalid_query, no_emails | Fix the request (details are included when available). |
| 401 | unauthorized | Missing, wrong or revoked API key. |
| 402 | insufficient_credits | Not enough credits in pool for needed. Buy credits or use creditType: "auto". |
| 403 | api_key_read_only, email_not_verified, account_suspended | Use a read & write key, confirm the account email, or contact support. |
| 404 | task_not_found | No list with this id in your account. |
| 409 | not_processing, not_paused, not_cancellable | The list is not in a state that allows this action. |
| 410 | results_expired | The results were deleted after retentionDays days. |
| 413 | too_many_emails, payload_too_large | Split the list (max addresses per list, 25 MB per request). |
| 429 | rate_limited | Wait for retry-after seconds. |
| 502 | verification_failed | The check failed on our side; the credit was refunded. Try again. |
MCP and integrations
- MCP server for AI assistants (Claude, Cursor, VS Code…):
https://api.mailquell.com/mcp, with toolsverify_email,verify_list,get_list,list_listsandget_credits. Same credits as the API. Setup in Developer Hub → Integrations. - No code: WordPress and WooCommerce plugin, a form widget for Wix and Ghost, Zapier and n8n, Mailchimp, Brevo and MailerLite list sync, and a Chrome extension.
- Questions about the API: support@mailquell.com.