Skip to main content

Validate email addresses before you send

The deliverability-health guide covers what the traffic you have already sent says about a contact. This guide is the other half: what to check about an address before the first send, when you have no history to rely on. Two endpoints cover it — a single-address check (POST /api/v1/email/validate) and a batch scrub (POST /api/v1/email/validate/bulk) — and this page shows where each one belongs in a launch workflow.

1. Why scrub before you send

Every hard bounce you mail chips away at sender reputation, and every dead address costs a quota slot and a delivery attempt. Scrubbing a list before the campaign means you only pay for addresses that can actually receive mail, and a complaint-prone row (a disposable inbox, a role mailbox) gets a human decision instead of an automated one. That matters most while your domain is still warming up: the ramp schedule in the email lifecycle guide is sized for engaged, working recipients. Feeding a run of hard bounces into an early warmup day degrades the deliverability signal the ramp reads to adjust today’s cap, so you slow down exactly at the moment you were trying to speed past. Scrub first; ramp clean traffic.

2. Pre-flight sweep — single addresses at the capture edge

Reject obvious typos where the address enters, not after it reaches your CRM. Call POST /api/v1/email/validate from your signup form or edge worker and decide the row before you import it.
The verdict blocks on two signals: a valid: false address (a definitive no-MX domain or broken syntax) is rejected outright, and a suggested_correction either re-prompts the user with the fix or corrects the row before it lands in the contact list. A mx_found: null result (a DNS resolver timeout) is not a rejection — validation treats it as unknown and keeps the address eligible for a retry a few seconds later. The single-address endpoint also needs the authentication you would attach to a form. If the capture point is a public page, run the check through your own proxy with the API key held server-side — the same pattern your web hooks already use.

3. Bulk scrub before a campaign

For an existing segment — a CSV you are importing or an audience you are about to launch — send the whole list in one call to POST /api/v1/email/validate/bulk (up to 1,000 addresses per call, de-duplicated case-insensitively). Slot this into the send gate between the audience selection and the suppression check described in the pre-flight checklist: the scrub is where a row fails before it costs anything.
Read the summary first: it is the gate, not the per-row detail. A total far above your import count means the list de-duplicated correctly; a non-zero invalid means at least one row failed definitively; risky counts deliverable-but-flagged rows (possible typo, disposable, role account) that want a human decision. Only after the summary passes do you walk the per-row results to fix or drop the offending rows. Chunk longer lists into sequential calls of up to 1,000 addresses, and do not parallelize: the batch endpoint is rate-limited per tenant (see the error section below), so a fan of parallel requests just burns the same quota faster. Input order is preserved across the results rows, so map them back to your CSV one row at a time.

4. Validation and suppression order

Validation and the suppression list answer different questions, and the order matters:
  1. Validate first. The scrub reads only the address itself — it decides “can this destination receive mail at all?”. Run it on every row you are about to accept.
  2. Then check suppression. A clean verdict does not mean the address is sendable — the deliverable address may still be on your blocklist (a hard bounce, complaint, unsubscribe, or manual entry). Check the row against the Email → Suppressions list via GET /api/v1/email/suppressions (paged entries, with search and reason filters), and remove what returns a hit before the send gate decides for itself.
The same order runs on the send path too: validation decides the destination is reachable, then the org’s blocklist is applied recipient-by-recipient. If you skip step 1, bounce-suppression entries only accumulate — the send gate cannot turn a dead destination into a clean list.

5. What the verdict actually reports

Only what the endpoint can honestly derive gets returned — these are the verdict buckets the API really answers, and no SMTP mailbox probing means no claim of “this mailbox exists”.
  • invalid_syntax — the address failed dot-atom validation (missing @, a malformed label, a non-2-letter TLD). Reject; a corrected suggestion may be lower-cost than a bounce.
  • no_mx_record — the domain published neither a usable MX nor an A/AAAA fallback (RFC 5321 §5.1), or published an RFC 7505 null MX declaring it will not accept mail. A definitive verdict, not a guess. Drop these rows.
  • disposable_domain — the domain is a known throwaway provider. Deliverable, but complaints-prone on marketing traffic. Review or re-permission.
  • role_account — the local part is a group mailbox (info@, support@, sales@, …). Deliverable, but engagement is low and complaint risk is high. Review before marketing sends.
  • possible_typo — a near-miss domain correction was found. suggested_correction carries the fix; apply or confirm it.
  • mx_unknown — the MX lookup timed out. Validation does not treat a timeout as a verdict; the row stays eligible and should be retried.
  • deliverable — none of the above. Sendable subject to the suppression check in section 4.
Two reading rules: valid is syntax-driven and only flips false on a definitive no-MX; mx_verified is stricter and only flips true on a definitive MX. A row can be valid: true with mx_verified: false when the lookup was unknown — treat that as an honest unknown, not a failure.

6. Common errors

  • Empty batch → 422. POST /email/validate/bulk rejects an empty emails array with 422 VALIDATION_ERROR and an issues list pointing at the missing rows. Send at least one address per call.
  • Too many addresses → 422. The array cap is 1,000 addresses per bulk call; longer lists must be chunked into sequential requests.
  • Rate limit → 429. Bulk scrubs share a tight per-tenant bucket (5 calls per minute on POST /email/validate/bulk, sized for list imports, not refresh loops). Back off when the response carries retry_after (or a Retry-After header, on suppression-list endpoints) and retry.
  • Suppression-list reads → 429. The /suppressions page/export routes run on the per-route read budget; a 429 there means pause rather than hammering the same filter.
Details of each error code and its remediation stay in the error codes reference.

Next steps