API Documentation
Integrate email verification into your app in minutes. No API key required - quota is tracked by IP.
Endpoints
/api/check-emailVerify a single email address/api/quotaGet your remaining daily quota/api/bulk-check-emailSubmit a bulk verification job (max 100 emails)/api/verification-result/:job_idPoll a bulk job resultCheck a single email
/api/check-emailRequest body
| Field | Type | Description |
|---|---|---|
| string | The email address to verify | |
| validation_type | string | smtp · mx · mx_blacklist · regex — required. Any other value returns 400. |
Example
curl -X POST https://www.mailtruster.com/api/check-email \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "validation_type": "smtp"}'Response · 200
{
"success": true,
"data": {
"email": "marie.durand@example.com",
"validation_type": "smtp",
"result": "valid",
"details": null,
"reasons": [],
"checks": {
"syntax": true,
"dns": true,
"smtp_port": true,
"smtp_connection": true,
"smtp_rcptto": true,
"smtp": true
},
"reason": null,
"errors": null,
"role": false,
"spamtrap": false,
"disposable": false,
"free": false,
"catch_all": false,
"protected": false,
"metadata": {
"validated_at": "2026-08-10 10:15:51 +0000",
"smtp_host": "203.0.113.10"
}
},
"meta": {
"request_id": "018e6b3c-...",
"timestamp": "2026-08-10T10:15:51+00:00",
"version": "v1"
}
}success tells you the request worked. It says nothing about the address — that is data.result. A 200 carrying "result": "invalid" is a perfectly successful request.
The verdict · data.result
Always present, always one of these four. Handle all four — a shortcut such as ["valid","risky"].includes(result) loses the distinction that matters most.
validThe mailbox exists and nothing suspicious was found. Send.riskyA signal makes the address risky. Depending on the reason the mailbox is known to exist (catch_all, role) or not (spamtrap, disposable). Send with caution, or exclude from mass campaigns.invalidEstablished refusal: bad syntax, no MX, or the mailbox does not exist. Do not send. The contact can be dropped.unknownNothing could be established — the server blocked, greylisted or dropped the connection. Do not conclude, schedule another check, and do NOT delete the contact.invalid. Filing it under invalid deletes good contacts because a remote server was protective.switch (data.result) {
case "valid": return send();
case "risky": return sendWithCaution(); // look at data.reasons
case "invalid": return dropContact();
case "unknown": return scheduleRetry(); // DO NOT delete the contact
}Response fields
| Field | Type | Present | Description |
|---|---|---|---|
| string | always | The address, as returned upstream. | |
| validation_type | string | always | The type actually used. |
| result | string | always | The verdict — valid, risky, invalid or unknown. |
| details | string | null | always | Dominant reason, equal to reasons[0]. null when nothing was flagged. |
| reasons | string[] | always | Every reason, in precedence order. [] when none. |
| checks | object | always | Per-sub-check booleans. A check that does not apply is an absent key, never null. |
| role | bool | when known | Local part is a role prefix (contact@, admin@…). |
| spamtrap | bool | when known | Domain is a known spamtrap. |
| disposable | bool | when known | Domain is a throwaway provider. |
| free | bool | when known | Domain is a consumer mail provider. Commercial signal only — it does not affect the verdict. |
| catch_all | bool | null | smtp only | true = the server accepts any address. null = could not be established. |
| protected | bool | null | smtp only | true = the server blocks us on connection or reputation. When true, result is unknown. |
| reason | string | null | always | Upstream failure_reason: the uppercase error code of the first error. Not the reason vocabulary — use details and reasons for that. |
| errors | array | null | always | A list of { code, message }, or null. |
| metadata | object | when non-empty | Non-business info: validated_at, mx_records, smtp_host, smtp_error… |
An absent flag is not false. role, spamtrap, disposable and free are omitted when they were never determined — they are never null and never guessed. In JavaScript if (data.disposable) reads a missing key as falsy and silently reports not disposable about an address that was never checked. Test presence: 'disposable' in data ? data.disposable : null.
catch_all and protected are emitted if and only if validation_type is smtp, and in that case they are always present, including with the value null. Here null means an SMTP session happened but was inconclusive, whereas absent means the question was never asked. Do not collapse the two, and do not treat catch_all: null as false.
details and reasons values
details is the first entry of reasons, which holds them all in precedence order. This vocabulary is open and may grow — always handle an unrecognised value.
invalidsyntax_error · no_mx_record · mailbox_not_foundriskyspamtrap · disposable · catch_all · roleunknowngreylisted · sender_blocked · timeout · connection_dropped · port_closed · mailbox_full · no_smtp_data · inconclusive · protected · throttled · rate_limitedvalidnull — no signal at allTwo orderings worth knowing: spamtrap and disposable outrank unknown (they hold whether or not the mailbox exists), while catch_all and role rank below it (they assume a completed SMTP session). And mailbox_full is unknown, not invalid — the mailbox exists and will become deliverable again. reasons stays exhaustive: a role address that timed out gives result: "unknown", details: "timeout", reasons: ["timeout", "role"].
When the service is saturated
Two distinct events come back as result: "unknown" with checks: {} and an HTTP 200 — never a 502. The four address flags are absent from these responses, so render them as not determined rather than as no.
throttledThe verification queue is saturated. It drains on its own, so waiting helps. On the bulk path this appears only after automatic retries are exhausted, several minutes in.rate_limitedA per-IP rate limiter refused the request. This one is never retried, so it can settle within seconds of queueing and usually hits a whole batch at once.Treat both like any other unknown: nothing was established, retry later. The address itself is untouched.
Validation types
smtpSyntax + DNS + SMTP session. A valid here means the mailbox exists and nothing was flagged. The only type that tests the mailbox itself.mxSyntax + MX record lookup. A valid here means the domain can receive mail — NOT that this address exists.mx_blacklistMX check + MX blacklist lookup. A valid here means the MX is not blacklisted — NOT that this address exists.regexSyntax only, no network call. A valid here means the syntax is well-formed — NOT that the domain exists.A verdict only covers what the type checked. Only smtp opens an SMTP session, so for the other three details is null and reasons is []. The address flags are computed for every type — they need no network — but they only influence the verdict in smtp. To spot a throwaway or a role address outside of smtp, read the flags rather than the verdict.
Get quota
/api/quotaExample
curl https://www.mailtruster.com/api/quota
Response · 200
{
"daily_limit": 100,
"used": 14,
"remaining": 86
}Bulk verification
Async workflow - submit a list, get a job_id per email, then poll each one individually.
1 - Submit a job
/api/bulk-check-emailcurl -X POST https://www.mailtruster.com/api/bulk-check-email \
-H "Content-Type: application/json" \
-d '{
"emails": ["alice@example.com", "bob@test.com"],
"validation_type": "mx"
}'Maximum 100 emails per request. Returns 202 Accepted. Nothing is verified yet.
{
"success": true,
"data": {
"results": [
{
"email": "ok@example.com",
"status": "queued",
"message": "Queued for verification",
"job_id": "018e6b3c-1111-..."
},
{
"email": "oops",
"status": "error",
"message": "Invalid email format",
"job_id": "018e6b3c-2222-..."
}
]
},
"meta": { ... }
}Every input line gets one entry and its own job_id — there is no batch-level id, poll one result per address. Addresses are verified independently and in no guaranteed order, so results arrive out of order.
A malformed address is rejected immediately with status: "error", is never queued, and costs no quota. Its job_id will never resolve — polling it returns 404 forever. Filter those out of your polling set using this response. Here: poll 018e6b3c-1111-..., never 018e6b3c-2222-....
Quota is consumed at submission, for the count of valid addresses only. A bulk request is all or nothing: if that count exceeds what remains, the whole request is refused with 429 and nothing is queued.
2 - Poll a result
/api/verification-result/:job_idcurl https://www.mailtruster.com/api/verification-result/018e6b3c-1111-...
data.result is the record, and it has exactly three keys: email, status and verification. While the job is running there is nothing to serve:
{
"success": true,
"data": {
"job_id": "018e6b3c-1111-...",
"result": {
"email": "ok@example.com",
"status": "processing",
"verification": null
}
},
"meta": { ... }
}Once it is finished, verification is exactly the payload of the single check above, key for key — so the same parsing code handles both endpoints:
{
"success": true,
"data": {
"job_id": "018e6b3c-1111-...",
"result": {
"email": "ok@example.com",
"status": "done",
"verification": {
"email": "ok@example.com",
"validation_type": "smtp",
"result": "valid",
"details": null,
"reasons": [],
"checks": { "syntax": true, "dns": true, "smtp": true },
"reason": null,
"errors": null,
"role": false,
"spamtrap": false,
"disposable": false,
"free": false,
"catch_all": false,
"protected": false,
"metadata": { "validated_at": "..." }
}
}
},
"meta": { ... }
}status is not the verdict
Two different questions, kept apart. data.result.status answers where is the processing; data.result.verification.result answers what is the verdict. status only ever takes these four values:
queuedAccepted, waiting in the queue. Keep polling, show as pending.processingA worker is on it, or a retry is pending. Keep polling, show as pending.doneFinished successfully — terminal. Stop polling and read verification. It says the check completed, not that the address is good.errorThe check itself failed for technical reasons — terminal. Stop polling, verification is null, offer a retry. This is NOT a verdict about the address: do not show it as invalid.result values, and status will never take them. Waiting for status === "valid" polls forever.const FINAL_STATUSES = ["done", "error"]; // and nothing else
const { status, verification } = res.data.result;
if (status === "queued" || status === "processing") return pending();
if (status === "error") return technicalFailure(); // verification is null
// status === "done" — verification is the single-check payload
switch (verification.result) {
case "valid": return good();
case "risky": return warn(); // look at verification.reasons
case "invalid": return bad();
case "unknown": return inconclusive(); // do NOT delete the contact
}Polling practice
Counting for a summary
Count on verification.result, over the records whose status is done. Records in error have no verdict and belong in their own bucket — folding them into invalid misreports technical failures as bad addresses. A defensible breakdown has five buckets: valid, risky, invalid, unknown, error.
Error codes
An error answers with "success": false and an error object holding code, message and, when there is something to add, details.
{
"success": false,
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Daily verification limit reached",
"details": { "limit": 100, "remaining": 0, "requested": 1 }
},
"meta": { ... }
}EMAIL_REQUIRED400Missing, empty or malformed email fieldINVALID_VALIDATION_TYPE400validation_type outside the allowed list - details.allowed lists themBULK_LIMIT_EXCEEDED422More than 100 emails in a single bulk request - details carries limit and receivedQUOTA_EXCEEDED429Daily limit reached - details carries limit, remaining and requestedJOB_NOT_FOUND404No verification record for that job_idUPSTREAM_ERROR502The verification service is unreachable or erroredUPSTREAM_SCHEMA_MISMATCH502The verification service answered without a usable result verdictINTERNAL_ERROR500Unexpected server errorRequests rejected before they reach the verification service answer with a shorter envelope — { "error": { "code", "message" } }, no success and no meta — and their own codes: E1002 (invalid email or emails array), E1003 (invalid or missing validation_type), E1006 (missing job_id), E1004 and E1005 (unreadable or unavailable upstream), E1001 (server misconfigured). Handle both shapes: test error.code without assuming it is one of the names in the table above.