API Reference

Ship MailSift with predictable endpoints.

Everything you need to integrate single checks and bulk jobs into your stack. Authenticate with a single header, ship in minutes.

Base URL
PRODUCTION API
https://mailsift.dev/api/v1
All requests start here · v1 stable

API Key Auth#

Send your key in the X-API-Key header for server-to-server verification requests. Generate a key from your dashboard.

Header:X-API-Key: msk_…
Quickstart

Copy, call, ship.#

Start with a single verify call, then move into keys, jobs, and parsed result rows as soon as you need scale. The examples below match the current route surface.

curl "https://mailsift.dev/api/v1/[email protected]" \
  -H "X-API-Key: msk_your_api_key_here"
Check Your Balance
curl "https://mailsift.dev/api/v1/account" \
  -H "X-API-Key: msk_your_api_key_here"
Deep Verify (mailbox check over SMTP)
curl "https://mailsift.dev/api/v1/[email protected]&mode=deep" \
  -H "X-API-Key: msk_your_api_key_here" \
  -H "Idempotency-Key: 3f1c9a7e-5d40-4c1b-9c2e-0a7b6d3e8f12"
Batch Verify (up to 100)
curl -X POST "https://mailsift.dev/api/v1/verify/batch" \
  -H "X-API-Key: msk_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"emails":["[email protected]","[email protected]","[email protected]"]}'
Create Bulk Job
curl -X POST "https://mailsift.dev/api/v1/verify/bulk" \
  -H "X-API-Key: msk_your_api_key_here" \
  -F "[email protected]"
Poll Job Status
curl "https://mailsift.dev/api/v1/jobs/{job_id}" \
  -H "X-API-Key: msk_your_api_key_here"
Verify a Webhook Signature (Node.js)
// req.body — raw body string, signature — X-MailSift-Signature header
const crypto = require('crypto');
const expected = crypto
  .createHmac('sha256', webhookSecret)  // from GET /account
  .update(req.body)
  .digest('hex');
const valid = crypto.timingSafeEqual(
  Buffer.from(expected),
  Buffer.from(signature)
);
Endpoints

Full route map#

Verification

2 endpoints
GET
/api/v1/verify?email={email}&mode={fast|deep}
Single email verification. Optional mode; fast is the default.
POST
/api/v1/verify/batch
Verify up to 100 emails synchronously. Optional mode body field.

Account

1 endpoint
GET
/api/v1/account
Credits balance, usage this month, webhook secret.

Bulk Jobs

7 endpoints
POST
/api/v1/verify/bulk
Upload CSV and create a bulk job. Optional mode form field.
GET
/api/v1/jobs
List your jobs.
GET
/api/v1/jobs/{id}
Job summary and counts.
GET
/api/v1/jobs/{id}/stream
Server-sent events progress stream.
GET
/api/v1/jobs/{id}/results
Paginated parsed result rows.
GET
/api/v1/jobs/{id}/download
Download final CSV.
DELETE
/api/v1/jobs/{id}
Cancel or delete a job.
Response shape

Single verify JSON#

Single verify returns the classification, numeric score, boolean checks, and enrichment fields used by the dashboard. Omitting mode gives you exactly this, at one credit.

{
  "email": "[email protected]",
  "status": "valid",
  "score": 92,
  "mode": "fast",
  "credits_used": 1,
  "checks": {
    "syntax": true,
    "free": true,
    "disposable": false,
    "mx": true,
    "relay": false,
    "spf": true,
    "dmarc": true,
    "role": false,
    "catch_all": false
  },
  "domain": "gmail.com",
  "mx_records": [
    "gmail-smtp-in.l.google.com",
    "alt1.gmail-smtp-in.l.google.com"
  ],
  "normalized_email": "[email protected]",
  "mx_provider": "Google",
  "is_catch_all": false,
  "domain_age_days": 11188,
  "response_time_ms": 46,
  "checked_at": "2026-04-01T17:32:18Z"
}

Deep verify JSON#

A deep response is the same document plus an smtp object holding what the recipient mail server said about the mailbox. Nothing is renamed or removed.

{
  "email": "[email protected]",
  "status": "valid",
  "score": 97,
  "mode": "deep",
  "credits_used": 5,
  "checks": {
    "syntax": true,
    "free": true,
    "disposable": false,
    "mx": true,
    "relay": false,
    "spf": true,
    "dmarc": true,
    "role": false,
    "catch_all": false
  },
  "smtp": {
    "checked": true,
    "status": "deliverable",
    "response_code": 250,
    "enhanced_code": "2.1.5",
    "catch_all": false,
    "tls": true,
    "mx_host": "gmail-smtp-in.l.google.com",
    "provider": "google",
    "latency_ms": 84
  },
  "domain": "gmail.com",
  "mx_provider": "Google",
  "is_catch_all": false,
  "domain_age_days": 11188,
  "response_time_ms": 612,
  "checked_at": "2026-04-01T17:32:18Z"
}
Bulk results

Parsed job rows#

Use the parsed result endpoint to drive in-app filtering and dashboards without downloading the whole CSV first.

{
  "results": [
    {
      "email": "[email protected]",
      "verification_status": "risky",
      "verification_score": 82,
      "is_disposable": false,
      "has_mx": true,
      "is_role_based": false,
      "is_free_provider": true,
      "is_relay": false,
      "is_blacklisted": false,
      "has_spf": true,
      "has_dmarc": true,
      "is_catch_all": false,
      "domain_age_days": 11188,
      "mx_provider": "Google",
      "normalized_email": "[email protected]",
      "did_you_mean": ""
    }
  ],
  "total_rows": 16,
  "page": 1,
  "page_size": 50,
  "total_pages": 1
}
Deep Verification

Check the mailbox, not just the domain.#

Every verification endpoint takes an optional mode. Omitting it means fast, so existing integrations are unchanged: same latency, same one-credit cost, same response fields. Standard verification is quick. Deep verification takes longer, because MailSift opens a connection to the recipient mail server and waits for its replies, and that server's speed is outside our control. We do not promise an exact completion time for it.

mode=fast
1 credit per email

The default engine, unchanged. Everything MailSift can learn without contacting the recipient mail server.

Syntax, normalization, domain and DNS, MX, disposable and temp-mail detection, relay and forwarder detection, role accounts, free providers, SPF, DMARC, typo suggestions, domain intelligence, domain age, local-part risk signals, catch-all detection, and the MailSift trust score.

mode=deep
up to 5 credits per email

Everything fast does, plus an SMTP conversation with the recipient mail server to check the mailbox itself.

Mailbox acceptance, recipient-specific rejection, catch-all resolution, greylisting detection, provider-specific interpretation, and STARTTLS. Deep responses carry an extra smtp object.

Where mode goes#

Optional everywhere. Enum fast or deep, default fast.

GET /api/v1/verify
Query parameter
POST /api/v1/verify/batch
JSON body field
{ "emails": [...], "mode": "deep" }
POST /api/v1/verify/bulk
Multipart form field
-F "[email protected]" -F "mode=deep"
!Responses gain three additive fields
mode and credits_used on every verification, and smtp on deep responses only. Batch responses also report mode; a bulk job adds mode and credits_reserved. Nothing existing was renamed or removed.

Credits#

One shared balance covers both modes. There is no separate SMTP subscription.

mode=fast1 credit
mode=deep, SMTP was unnecessary (invalid syntax, nonexistent domain, no MX, null MX)1 credit
mode=deep, SMTP could not produce a useful answer (timeout, throttling, temporary policy block, MailSift infrastructure problem)1 credit
mode=deep, SMTP produced real mailbox- or domain-level intelligence5 credits
Overall status is unknown0 credits
!One price per request, never both
A deep request costs 1 or 5 credits for the whole call. The standard 1-credit verification is not charged on top of the 5-credit deep price. An inconclusive overall result still costs 0 credits, as it always has.
!Reserve and refund
A deep request needs at least 5 credits available before it starts. That amount is held up front and the unused remainder is refunded once the real cost is known. A bulk deep job reserves 5 credits per row the same way.

Reason codes and signals#

Branch on sub_status instead of parsing the sentences in reasons. A check that did not run is left out of checks rather than reported as false, so false always means checked and clean.

Fields
sub_statusStable code for the most decisive finding behind a status that is not valid. Omitted for valid addresses. Values may be added over time but are never renamed.
catch_all_confidenceInteger from 5 to 95: how likely the mailbox is to be real when the domain accepts every recipient, weighed from domain age, email authentication, mail provider and the shape of the local part. Present only for catch-all domains. The catch-all score penalty scales with it: 5 points at 70 or above, 15 at 40 to 69, 25 below 40.
dmarc_policyThe DMARC policy that applies to the domain: none, quarantine or reject. Taken from the organizational domain (honoring sp=) when the domain itself publishes no record.
checks.dnsblDomain or primary mail server is on a blocklist. Present only when the blocklist check produced an answer: false means checked and clean, and an absent key means the check did not run.
checks.mx_routableAt least one MX host resolves to a public address. Omitted when a resolver failure kept it from being determined.
checks.forwarderMail for the domain is handled by a forwarding service (ImprovMX, Forward Email, Cloudflare Email Routing, Namecheap forwarding). Context only; it does not affect the score.
checks.smtputf8The local part contains non-ASCII characters (RFC 6531). Such addresses are valid but not every mail system can deliver to them, and deep verification reports smtp_smtputf8 for them.
checks.mta_stsThe domain publishes an MTA-STS record. Context only; omitted when the lookup failed.
checks.tls_rptThe domain publishes a TLS-RPT record. Context only; omitted when the lookup failed.
checks.bimiThe domain publishes a BIMI record. Context only; omitted when the lookup failed.
sub_status values
failed_syntax_checkinvalid. The address is not well-formed.
domain_not_foundinvalid. The domain does not exist.
does_not_accept_mailinvalid. The domain publishes a null MX: it declares that it accepts no mail.
mx_unroutableinvalid. The domain's MX records name hosts that do not exist, or that resolve only to loopback or private addresses.
mailbox_not_foundinvalid. Deep only. The receiving mail server rejected the mailbox as nonexistent.
mailbox_disabledinvalid. Deep only. The mailbox exists but has been disabled or suspended.
domain_rejected_by_mail_serverinvalid. Deep only. The receiving server does not handle mail for this domain.
dns_errorunknown. A temporary DNS failure kept the domain from being resolved. Retry.
no_mx_recordsrisky. The domain exists but publishes no MX records, so deliverability cannot be confirmed.
disposablerisky. Throwaway address provider.
blocklistedrisky. The domain or its mail server is on a blocklist. See dnsbl_listings.
parkedrisky. The domain is parked or for sale.
role_basedrisky. A shared functional inbox such as info@ or support@.
role_based_catch_allrisky. A role address on a domain that accepts every recipient.
relayrisky. A privacy alias service that hides the real recipient.
catch_allrisky. The domain accepts every recipient, so this mailbox cannot be confirmed. See catch_all_confidence.
synthetic_local_partrisky. The local part looks machine-generated or like a test address, on a free provider.
new_domainrisky. The domain was registered less than 30 days ago.
low_scorerisky. No single finding; the combined score is below 80.

The smtp object#

Present on deep responses only. When no SMTP conversation ran, the object is { "checked": false, "status": "not_checked", "reason": "…" }.

!smtp.status is not the overall status
This is the most common integration mistake. smtp.status only describes what the recipient mail server said about the mailbox. The top-level status stays valid | risky | invalid | unknown and combines every signal. An address can be SMTP-deliverable and still be overall risky because it is disposable, a role account, or a relay. Branch your logic on the top-level status.
Fields
checkedWhether an SMTP conversation actually took place.
statusMailbox-level outcome. Not the overall status.
reasonWhy no conversation ran. Present only when checked is false.
sub_reasonExtra detail behind the status. Optional.
response_codeSMTP reply code, for example 250 or 550.
enhanced_codeEnhanced status code, for example "5.1.1".
catch_allWhether the domain accepts every recipient. Omitted entirely when the catch-all state is unknown, so a missing field is not the same as false.
tlsWhether the conversation used STARTTLS.
mx_hostMail host that answered.
providerRecognized mail provider, for example "google".
latency_msTime the SMTP conversation took.
smtp.status values
deliverableThe server accepted the recipient and its acceptance is trustworthy.
undeliverableThe server issued a recipient-specific permanent rejection.
catch_allThe domain accepts every recipient, so acceptance proves nothing about this mailbox.
unknownThe conversation completed but the answer is not conclusive, usually because the provider conceals mailbox existence.
greylistedA temporary deferral typical of greylisting. A later retry usually succeeds.
rate_limitedThe server is throttling right now.
policy_blockedThe server rejected on policy or reputation grounds aimed at the sender, not the recipient.
timeoutThe server did not answer in time.
not_checkedNo SMTP conversation was attempted. See reason.
smtp.reason values (only when checked is false)
smtp_not_requiredThe standard checks already settled the address.
smtp_not_requestedThe caller asked for fast mode.
smtp_unavailableThe SMTP layer was not available for this request.
smtp_no_mxThe domain has no usable MX record.
smtp_invalid_syntaxThe address is not well-formed.
smtp_smtputf8The local part is internationalized (RFC 6531). The probe does not negotiate SMTPUTF8, so it does not ask.
smtp_unsafe_mxThe MX record resolved to an address MailSift will not connect to.
smtp_throttledNo probe slot was available in time.
smtp_abuse_throttledThe caller exceeded mailbox-enumeration limits.
smtp.sub_reason values (optional)
mailbox_not_foundmailbox_fullmailbox_disableddomain_not_foundaccept_all_gatewayprovider_unverifiableconnection_failedprotocol_errortls_requiredsender_rejected
!What deep verification cannot prove
Deep verification confirms a mailbox where the receiving mail server supports it. Some providers deliberately conceal mailbox existence: Yahoo and AOL accept recipients they cannot confirm, and secure gateways such as Proofpoint and Mimecast accept every recipient at the perimeter. For those, MailSift returns an honest unknown, usually with catch_all set to true, instead of guessing.

Idempotency#

Send an optional Idempotency-Key header on GET /api/v1/verify so a retry after a network blip cannot charge you twice or start a second SMTP conversation. Without the header, behavior is exactly as before.

  • Same key, same request: the stored response is replayed. The verification does not run again and you are not charged again.
  • A replayed response carries an Idempotent-Replay: true header.
  • Same key, different request: 422.
  • Same key while the original request is still in flight: 409. Retry shortly.
  • Stored responses last 24 hours.

Deep bulk jobs#

Send mode=deep alongside the file and every row in the job gets a mailbox check. The job reserves 5 credits per row when it is created and refunds the unused remainder when it finishes.

curl -X POST "https://mailsift.dev/api/v1/verify/bulk" \
  -H "X-API-Key: msk_your_api_key_here" \
  -F "[email protected]" \
  -F "mode=deep"
!Two extra CSV columns
A deep job appends smtp_status and mailbox_exists (yes, no, or unknown) after all existing columns. Existing column positions are unchanged, so a parser reading by index keeps working.
Worked examples

Seven responses, end to end#

Fields not relevant to the point being made are trimmed from these responses.

1 · Fast, valid

Fast responses never include an smtp object.

{
  "email": "[email protected]",
  "status": "valid",
  "score": 92,
  "mode": "fast",
  "credits_used": 1,
  "checks": { "syntax": true, "mx": true, "disposable": false,
              "role": false, "catch_all": false },
  "domain": "gmail.com",
  "mx_provider": "Google",
  "response_time_ms": 46
}
2 · Fast, invalid

The domain has no MX record, so no mail can reach it.

{
  "email": "[email protected]",
  "status": "invalid",
  "score": 0,
  "mode": "fast",
  "credits_used": 1,
  "checks": { "syntax": true, "mx": false, "disposable": false,
              "role": false },
  "domain": "no-such-domain-9x8y.com",
  "mx_records": [],
  "reasons": ["domain has no MX records"],
  "response_time_ms": 38
}
3 · Deep, deliverable

The mail server accepted this recipient and its acceptance is trustworthy. Billed at the deep rate.

{
  "email": "[email protected]",
  "status": "valid",
  "score": 97,
  "mode": "deep",
  "credits_used": 5,
  "smtp": {
    "checked": true,
    "status": "deliverable",
    "response_code": 250,
    "enhanced_code": "2.1.5",
    "catch_all": false,
    "tls": true,
    "mx_host": "gmail-smtp-in.l.google.com",
    "provider": "google",
    "latency_ms": 84
  },
  "domain": "gmail.com",
  "response_time_ms": 612
}
4 · Deep, undeliverable

A recipient-specific permanent rejection. The mailbox does not exist.

{
  "email": "[email protected]",
  "status": "invalid",
  "score": 4,
  "mode": "deep",
  "credits_used": 5,
  "smtp": {
    "checked": true,
    "status": "undeliverable",
    "sub_reason": "mailbox_not_found",
    "response_code": 550,
    "enhanced_code": "5.1.1",
    "catch_all": false,
    "tls": true,
    "mx_host": "gmail-smtp-in.l.google.com",
    "provider": "google",
    "latency_ms": 96
  },
  "domain": "gmail.com",
  "reasons": ["mail server rejected this recipient"],
  "response_time_ms": 640
}
5 · Deep, catch-all

The domain accepts every recipient, so acceptance proves nothing about this mailbox. Overall status is risky, not valid.

{
  "email": "[email protected]",
  "status": "risky",
  "score": 58,
  "mode": "deep",
  "credits_used": 5,
  "smtp": {
    "checked": true,
    "status": "catch_all",
    "sub_reason": "accept_all_gateway",
    "response_code": 250,
    "catch_all": true,
    "tls": true,
    "mx_host": "mx.catchall-corp.com",
    "provider": "other",
    "latency_ms": 219
  },
  "domain": "catchall-corp.com",
  "is_catch_all": true,
  "reasons": ["domain accepts all recipients",
              "role-based mailbox"],
  "response_time_ms": 750
}
6 · Deep, unknown

Yahoo accepted the recipient but does not confirm mailbox existence. MailSift reports that honestly instead of calling it deliverable. Still billed at the deep rate, because establishing that the domain accepts everything is a real finding.

{
  "email": "[email protected]",
  "status": "risky",
  "score": 61,
  "mode": "deep",
  "credits_used": 5,
  "smtp": {
    "checked": true,
    "status": "unknown",
    "sub_reason": "provider_unverifiable",
    "response_code": 250,
    "catch_all": true,
    "tls": true,
    "mx_host": "mta5.am0.yahoodns.net",
    "provider": "yahoo",
    "latency_ms": 302
  },
  "domain": "yahoo.com",
  "reasons": ["provider does not confirm mailbox existence"],
  "response_time_ms": 840
}
7 · Deep requested, SMTP unnecessary

The standard checks already settled the address, so no SMTP conversation ran and you pay the fast price.

{
  "email": "[email protected]",
  "status": "invalid",
  "score": 0,
  "mode": "deep",
  "credits_used": 1,
  "smtp": {
    "checked": false,
    "status": "not_checked",
    "reason": "smtp_not_required"
  },
  "domain": "no-such-domain-9x8y.com",
  "mx_records": [],
  "reasons": ["domain has no MX records"],
  "response_time_ms": 41
}
Bulk workflow

How bulk processing works#

  1. 1Upload a CSV with an email column using multipart form data.
  2. 2Receive a job id immediately and poll /jobs/{id} or stream /jobs/{id}/stream.
  3. 3Read parsed rows from /jobs/{id}/results for in-app filtering.
  4. 4Download the final CSV from /jobs/{id}/download when the job reaches complete.
!CSV requirements
Include a header row. The parser will look for columns like email, email_address, or any header containing "email".
Status semantics

What the statuses mean#

The top-level status is the same field in both modes and combines every signal MailSift gathered. Deep verification feeds the mailbox result into it; it does not replace it.

valid
Strong domain and mailbox-pattern signals. Good default pass case. In deep mode the recipient server also accepted the mailbox and its acceptance is trustworthy.
risky
Deliverable-looking domain but suspicious local part, role-based inbox, relay, or uncertain posture. Deep mode adds two cases here: a catch-all domain, and a provider that accepted the recipient without confirming the mailbox exists. A mailbox can be SMTP-deliverable and still land here.
invalid
Domain does not exist, null MX, or other hard failure. In deep mode this also covers a recipient-specific rejection from the mail server, such as a 550 mailbox_not_found.
unknown
Reserved for inconclusive processing failures or transient edge cases. Costs 0 credits in either mode. An smtp.status of unknown does not force this: it usually produces a risky address, because knowing the domain accepts everything is itself a finding.