EmailVerifyerAPI developer documentation

One REST surface, one frozen response shape, and one error vocabulary. Everything on this page is the contract the service is built and tested against, with runnable examples in fourteen languages.

API v1 Early access, launching Q3 2026
llms.txt

Getting started

Overview

The API answers one question well: can this address receive mail, and how confident are we? You send an address or a list, and you get back a verdict, the reason behind it, the individual checks that produced it, and a score you can threshold on.

Two surfaces exist. The public API is authenticated with an API key and is what your servers call. The session API backs the dashboard, uses cookies and CSRF, and is not intended for machine clients. This page documents the public API and describes the session API only so you know why you should not call it.

BASE URL

One host, one prefix

https://www.emailverifyerapi.com/api/public/v1

AUTH

Bearer API key

Authorization: Bearer mp_live_…, scoped per key.

FORMAT

JSON in, JSON out

UTF-8 only. Success is {"data":…}, failure is {"error":{…}}.

STATE

Early access

Keys are issued to early-access tenants. Request access.

What the API does

Capabilities of the v1 public API
CapabilityEndpointNotes
Verify one addressPOST /verifySyntax, MX, SMTP probe, disposable, role and free-provider checks in one call.
Verify a listPOST /batchesAsynchronous. Submit up to 500 addresses per request, poll, then page the results.
Read batch progressGET /batches/{id}Returns the job status from the locked job_status set.
Read batch resultsGET /batches/{id}/resultsCursor-paginated, 25 per page by default and 100 at most.

What the API deliberately does not do

  • It does not promise a mailbox exists. Catch-all domains accept every address, so the honest answer there is accept_all, not valid.
  • It does not send email. Verification stops at the SMTP RCPT stage and no message is ever delivered.
  • It does not score reputation or predict inbox placement. Those depend on your sending history, not on the address.
  • It does not store the addresses you verify longer than your retention setting allows. See security and data handling.

Free alternatives while you wait for a keyThe browser verifier runs entirely client-side, and the MX, SPF, DKIM and DMARC checkers need no account. None of them require this API.

Getting started

Quickstart

Three steps from nothing to a verdict. Pick your language once and every example on this page follows it.

  1. Create a key. In the dashboard, open API keys, choose the scopes you need, and copy the key. The full value is shown once, at creation.
  2. Put it in the environment, not in the source. The examples read EVA_API_KEY.
  3. Call POST /verify and branch on data.status.
verify.sh
curl -sS -X POST https://www.emailverifyerapi.com/api/public/v1/verify \
  -H "Authorization: Bearer $EVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'

The response

200 OK
{
  "data": {
    "email": "[email protected]",
    "status": "valid",
    "reason": "deliverable",
    "checks": {
      "syntax": true,
      "mx": true,
      "smtp": true,
      "disposable": false,
      "role": false,
      "free": false
    },
    "suggestion": null,
    "score": 0.98
  }
}

Read the code, not the proseBranch on status, reason and error.code. Human-readable message strings are free to change at any time and are never part of the contract.

Getting started

Authentication and API keys

Every public-API request carries an API key as a bearer token. There is no other authentication path: no query-string keys, no basic auth, no session cookies.

Required headers
Authorization: Bearer mp_live_9f2c1d7a4b8e0c3f5a6d2b9e1c4f7a0d3b6e9c2f5a8d1b4e
Content-Type: application/json
Accept: application/json

Key anatomy

API key format and storage
PropertyValueWhy it matters
Formatmp_live_<48 hex> or mp_test_<48 hex>The environment is visible in the key itself, so a test key can never reach production data by accident.
Display prefixFirst 11 characters, for example mp_live_9f2The dashboard and audit log show only the prefix. That is enough to identify a key and useless to an attacker.
StorageSHA-256 hash, matched in constant timeWe cannot show you a key again after creation, and a database disclosure does not leak usable credentials.
Statusactive, revoked, expiredRevocation is immediate. The next request with that key returns 401 invalid_api_key.

Scopes

Scopes are attached at creation and cannot be widened afterwards. Create one key per integration and give it the narrowest set that works.

API key scopes
ScopeGrantsTypical holder
verify:readPOST /verifyThe signup form and any single-address path.
verify:writePOST /batchesThe nightly list-cleaning job.
jobs:readGET /batches/{id} and its resultsDashboards and reporting jobs that read but never submit.

A key missing the required scope gets 403 insufficient_scope, never a silent empty result.

Operating rules

  • One key per environment and per service. Shared keys make the audit log useless and turn one rotation into an outage.
  • Rotate on a schedule and on every departure. Create the new key, deploy it, then revoke the old one. Revoking first causes downtime.
  • Revocation requires a fresh MFA challenge in the dashboard, and notifies every owner and admin on the tenant with the key prefix, the actor and the IP.
  • Never ship a key to a browser or a mobile binary. Anything a user can install, a user can read. Proxy the call through your own backend.

If a key leaksRevoke it in the dashboard first, then look at the audit log filtered by that key prefix to see what it touched. Rotating after the investigation leaves the window open longer than it needs to be.

Getting started

Test mode and going live

Keys are environment-tagged. A key beginning mp_test_ exercises the same routes, the same validation and the same response shape as a live key, and does not consume the plan's verification quota.

  • Write your integration tests against the contract, not against a live domain. Real mailboxes change; the response shape does not.
  • Assert on status and reason, and on the presence of the six checks keys. Do not assert on score to two decimal places: scoring inputs improve inside v1 without a version bump.
  • Test the failure paths. A missing scope, an expired key, a 429 and a 402 are all things production will hand you eventually.

Pre-launch checklist

Checklist before going live
CheckWhy
Retry policy covers 429 and 5xx onlyRetrying a 422 repeats a request the server will reject identically.
Client timeout above 5 seconds per addressThe SMTP probe alone has a 5-second budget. A 2-second client timeout guarantees false unknown results.
Unknown reason values do not crash the clientreason is an open enum: new values ship without a major version.
Keys read from configuration, never from source controlA key in a repository is a key in every fork of that repository.
Quota alerts wired to a channel a human reads402 quota_exceeded is not a retryable condition and stops verification dead.

Core concepts

Requests, responses and the envelope

Two response shapes exist across the whole API. Nothing else is ever returned, which means one deserialiser and one error handler cover every endpoint.

Envelope shapes
// Success, single resource
{ "data": { ... } }

// Success, list endpoint
{ "data": [ ... ], "next_cursor": "eyJpZCI6…" }   // next_cursor is null on the last page

// Failure, every status code
{ "error": { "code": "rate_limited", "message": "Too many requests." } }

// Failure with field detail, only for 422 validation_failed
{ "error": { "code": "validation_failed", "message": "…",
             "fields": { "email": ["must be a valid address"] } } }

Request conventions

Request conventions
ConventionDetail
EncodingUTF-8 JSON bodies. Internationalised addresses are accepted in Unicode form and normalised server-side.
Content typeapplication/json on every request with a body. A different type returns 400 bad_request.
MethodsGET reads and never changes state. POST creates. There are no PUT or DELETE routes on the public API.
TransportHTTPS only. Plain HTTP is redirected, and credentials sent over it should be treated as compromised.
TimeEvery timestamp is UTC, ISO 8601, with an explicit offset. Nothing is returned in local time.
IdentifiersResource ids are UUIDs, time-ordered where the service generates them. Treat them as opaque strings.
MoneyBilling figures are integer minor units with an explicit currency. Currencies are never converted into one another.

Forward compatibility, in one lineIgnore fields you do not know, tolerate new reason values, and never switch exhaustively on reason without a default branch. Added fields and added open-enum values ship continuously inside v1 and are not breaking changes.

Core concepts

The verification result object

One object shape is returned by POST /verify and by every row of a batch result page. It is frozen by contract tests: renaming or removing any key below requires a new major version.

Fields of the verification result object
FieldTypeMeaning
emailstringThe address as evaluated, normalised and lower-cased in the domain part.
statusenum, closedvalid, invalid, risky or unknown. A new value here would be a breaking change, so you can switch on it exhaustively.
reasonenum, openThe specific finding behind the status. New values may appear inside v1: always keep a default branch.
checks.syntaxbooleanThe address parses per RFC 5322 and has a plausible domain.
checks.mxbooleanThe domain publishes at least one usable MX record. See the MX checker for the detail behind this flag.
checks.smtpbooleanThe SMTP conversation completed. False means the probe was blocked, greylisted or timed out, not that the mailbox is missing.
checks.disposablebooleanThe domain belongs to a throwaway-mail provider.
checks.rolebooleanThe local part is a shared function such as info, sales or abuse rather than a person.
checks.freebooleanA consumer mailbox provider. Reported for segmentation only; it never changes the status.
suggestionstring or nullA likely correction for an obvious typo, for example gmial.com to gmail.com. Offer it, never apply it silently.
scorefloat 0.0 to 1.0Confidence, derived from the same table the status comes from. Stored as an integer 0 to 100 and divided by 100 on the way out.

Where the seven-value story wentInternally the service records a wider result set that separates disposable, role and accept_all. The API collapses all three into risky and keeps the detail in reason, so a client never has to handle two overlapping vocabularies.

Core concepts

Status, reason and score

Verdicts are deterministic. The table is evaluated top down and the first matching row wins, so the same inputs always produce the same output. This is the whole decision function, published rather than described.

Verification decision table
#Conditionstatusreasonscore
1Syntax failsinvalidsyntax0.00
2No usable MX recordinvalidno_mx0.00
3Disposable domainriskydisposable0.30
4SMTP accepted, not catch-all, not a role accountvaliddeliverable0.98
5SMTP accepted, role accountriskyrole0.60
6SMTP accepted every address, catch-allriskyaccept_all0.50
7SMTP completed and rejected the recipientinvalidmailbox_not_found0.05
8SMTP probe did not completeunknowntimeout0.40

Every reason value

Reason values and the action they imply
reasonWhat happenedDo this
deliverableThe receiving server accepted the recipient.Send.
mailbox_not_foundThe server rejected that specific address.Remove it. This is a hard bounce waiting to happen.
syntaxThe address is not a valid address.Reject at the form, and show suggestion if present.
no_mxThe domain cannot receive mail at all.Remove it, and check whether the domain is parked or misspelled.
disposableA throwaway provider, alive today and gone next week.Block at signup for paid products, allow for free trials if churn is acceptable.
roleA shared function mailbox, not a person.Keep for transactional mail, exclude from anything personalised, expect complaints if you market to it.
accept_allThe domain accepts every recipient, so nothing can be proven.Send to engaged contacts only, and watch the bounce rate for that domain.
greylistedThe server deliberately deferred the probe.Re-verify later. The address is not bad.
timeoutThe probe ran out of time.Re-verify later, or accept the address with a lower confidence.
full_inboxThe mailbox exists and cannot take more mail.Treat as temporary. It often clears.
blockedThe receiving server refused the probe itself.Inconclusive. Judge the address on engagement history instead.

Thresholds worth copying

  • Block at signup: status === "invalid". Nothing else is safe to block on without losing real customers.
  • Warn at signup: suggestion !== null. A typo correction recovers a signup that a hard block would have lost.
  • Exclude from a cold campaign: status !== "valid". Cold sending is where risky addresses cost you a domain reputation.
  • Keep in a warm campaign: status !== "invalid". An engaged contact on a catch-all domain is still a customer.

Core concepts

Errors

Error codes are stable identifiers. Messages are prose and may be reworded at any time. Branch on error.code, log error.message, and never parse a message string.

Error codes returned by the API
HTTPcodeCauseRetry?
400bad_requestMalformed body, wrong content type, or unparseable JSON.No, fix the request.
400bad_cursorA pagination cursor that was edited, truncated or reused from another resource.No, restart the walk.
401invalid_api_keyUnknown, revoked or expired key, or a missing Bearer prefix.No.
403insufficient_scopeThe key is valid but lacks the scope this route requires.No, mint a key with the right scope.
403forbiddenAuthenticated, and not allowed to touch this resource.No.
404not_foundNo such resource, or one belonging to another tenant. The two are indistinguishable on purpose.No.
402quota_exceededThe plan's included verifications are spent.No. Upgrade or wait for the period to roll over.
410endpoint_sunsetA deprecated surface reached its announced sunset date.No. The response carries the migration-guide URL.
422validation_failedInput failed validation. Carries a fields map.No, fix the input.
429rate_limitedToo many requests for this key. Sets Retry-After.Yes, after the stated delay.
500internal_errorSomething failed on our side. Logged with a correlation id; the message stays generic on purpose.Yes, with backoff.
422 Unprocessable Content
{
  "error": {
    "code": "validation_failed",
    "message": "The request could not be processed.",
    "fields": {
      "emails": ["must contain between 1 and 500 addresses"]
    }
  }
}

One rule that prevents most incidentsTreat 402, 401, 403 and 422 as permanent, and 429 plus 5xx as transient. A client that retries a 422 in a tight loop will burn a rate-limit budget it needs for real work.

Core concepts

Rate limits and quotas

Two independent ceilings apply. A rate limit caps how fast you may call. A quota caps how many verifications your plan includes. Hitting the first is a pacing problem; hitting the second is a billing decision.

Rate limits and quotas
CeilingApplies toValueOn exhaustion
Default request rateEvery public-API route, per key120 requests per 60 seconds429 rate_limited with Retry-After
Verification ratePOST /verify, per keyYour plan's requests-per-second allowance429 rate_limited with Retry-After
Batch sizePOST /batches500 addresses per request422 validation_failed
Page sizeResult pages25 by default, 100 maximumValues above 100 are clamped, not rejected
Included verificationsYour tenant, per billing periodSet by the plan; unused credits roll over on paid tiers up to one month's allowance402 quota_exceeded

Pacing that works

  • Rate-limit yourself first. A client-side token bucket set just below your allowance costs one small class and removes most 429s.
  • Prefer one batch to five hundred single calls. A batch is one request against the rate limit, and the work happens off your critical path.
  • Honour Retry-After exactly. It is the only authoritative wait. Fall back to exponential backoff with jitter when the header is absent.
  • Never run retries in an unbounded loop. Cap attempts, then surface the failure. Silent infinite retry is how one slow dependency becomes an outage.
  • Alert on 402 separately. It means work stopped for a commercial reason, and no amount of engineering fixes it.

Core concepts

Pagination

List endpoints are cursor-paginated. Cursors are opaque, stable under inserts, and cheap for the database, which is why there are no page or offset parameters to reach for.

Walking every page
GET /api/public/v1/batches/{id}/results?limit=100
  -> { "data": [ … 100 rows … ], "next_cursor": "eyJpZCI6IjAxOTIt…" }

GET /api/public/v1/batches/{id}/results?limit=100&cursor=eyJpZCI6IjAxOTIt…
  -> { "data": [ … 43 rows … ],  "next_cursor": null }      # done
  • Stop when next_cursor is null. Never infer the end from a short page: a full page can still be the last one.
  • Pass the cursor back verbatim. It is a signed, opaque token, not a row id. Editing it returns 400 bad_cursor.
  • Do not cache a cursor across runs. Cursors belong to the resource they came from and are not addresses you can bookmark.
  • limit is a maximum, not a promise. Handle a page shorter than you asked for.

API reference

Verify a single address

POST/api/public/v1/verifyverify:read

Runs the full check chain against one address and answers synchronously. This is the endpoint a signup form calls.

Body

Request body for POST /verify
FieldTypeRequiredNotes
emailstringRequiredOne address. Whitespace is trimmed; the domain is lower-cased. Anything that is not a single address returns 422 validation_failed.

Response

A verification result object inside data. The call blocks for the length of the check chain, and the SMTP probe alone has a five-second budget, so allow at least ten seconds of client timeout.

200 OK, a typo caught
{
  "data": {
    "email": "[email protected]",
    "status": "invalid",
    "reason": "no_mx",
    "checks": { "syntax": true, "mx": false, "smtp": false,
                "disposable": false, "role": false, "free": false },
    "suggestion": "[email protected]",
    "score": 0.0
  }
}

Errors specific to this route

  • 422 validation_failed with fields.email when the input is not one address.
  • 402 quota_exceeded when the plan's verifications are spent. The address is not consumed.
  • 403 insufficient_scope when the key lacks verify:read.

API reference

Create a batch

POST/api/public/v1/batchesverify:write

Queues a list for asynchronous verification and returns immediately with an id and a status. Work happens on the queue, so a slow receiving server never blocks your request.

Body

Request body for POST /batches
FieldTypeRequiredNotes
emailsarray of stringOne of1 to 500 addresses. Duplicates are verified once and reported once.
csvstringOne ofRaw CSV text with an address column, for callers that already hold a file.
202, queued
// Request
{ "emails": ["[email protected]", "[email protected]", "[email protected]"] }

// Response
{ "data": { "id": "0192a7f4-9c31-7c2e-8b55-2f6ad1c40e77", "status": "queued" } }

ChunkingSplit anything larger than 500 addresses client-side and submit several batches. Chunking is deliberate: it bounds a single request, keeps failures small, and lets you resume a large job without resubmitting the parts that already finished.

API reference

Read batch progress

GET/api/public/v1/batches/{id}jobs:read

Returns the job's current status. Poll it until the status leaves queued and running, then read the results.

Job status values
statusMeaningTerminal?
queuedAccepted and waiting for a worker.No, keep polling.
runningAddresses are being verified now.No, keep polling.
pausedHeld, usually because the tenant hit a quota mid-job.No, but polling will not move it. Resolve the cause first.
completedEvery address has a result.Yes.
failedThe job stopped after exhausting its retries.Yes. Partial results may still be readable.
cancelledStopped on request.Yes.

Poll like a good citizenFive seconds between polls is plenty, and every poll spends one request from the 120-per-minute budget. A one-second poll on ten concurrent jobs is 600 requests a minute, which rate-limits your own verification calls.

API reference

Read batch results

GET/api/public/v1/batches/{id}/resultsjobs:read

Returns one result object per address, cursor-paginated.

Query parameters for batch results
ParameterTypeRequiredNotes
limitintegerOptionalDefault 25, maximum 100. Higher values are clamped.
cursorstringOptionalThe next_cursor from the previous page, passed back unchanged.
200 OK, first page
{
  "data": [
    { "email": "[email protected]", "status": "valid", "reason": "deliverable",
      "checks": { "syntax": true, "mx": true, "smtp": true,
                  "disposable": false, "role": false, "free": false },
      "suggestion": null, "score": 0.98 },
    { "email": "[email protected]", "status": "risky", "reason": "role",
      "checks": { "syntax": true, "mx": true, "smtp": true,
                  "disposable": false, "role": true, "free": false },
      "suggestion": null, "score": 0.6 }
  ],
  "next_cursor": "eyJpZCI6IjAxOTJhN2Y0LTljMzEtN2MyZS04YjU1In0"
}

Results become readable as they are produced, so a long job can be consumed incrementally rather than waiting for completed. Rows are stable once written: re-reading a page returns the same verdicts.

Guides

Cleaning a list in bulk

The full flow: chunk, submit, poll, page, write. The example below is the same program in every language, and it is the one most integrations actually need.

  1. Chunk at 500. One request per chunk, and a failure costs you one chunk rather than the whole file.
  2. Submit and keep the id. Persist it before you start polling, so a crashed job can be resumed rather than resubmitted.
  3. Poll every five seconds until the status leaves queued and running.
  4. Page the results with limit=100 and follow next_cursor until it is null.
  5. Write the verdicts next to your own ids, not over the top of the addresses. You will want to audit the decision later.
batch.sh
# Create the batch, poll it, then page the results.
BATCH=$(curl -sS -X POST https://www.emailverifyerapi.com/api/public/v1/batches \
  -H "Authorization: Bearer $EVA_API_KEY" -H "Content-Type: application/json" \
  -d '{"emails":["[email protected]"]}' | jq -r '.data.id')

What to do with the verdicts

Bulk cleaning decisions by status
statusCold outreachExisting customers
validSend.Send.
invalidSuppress permanently.Suppress, and ask the customer for a new address in-product.
riskyExclude. This is where reputation damage comes from.Keep if they have opened mail in the last 90 days.
unknownRe-verify once, then exclude if still unknown.Keep. An inconclusive probe says nothing about a paying customer.

Do not delete on a single verdictSuppress rather than delete, and record which run produced the decision. A domain that was greylisting the probe on Tuesday can be answering normally on Wednesday, and deleted rows cannot be reviewed.

Guides

Validating a signup form

Real-time verification at signup is the highest-value integration and the easiest one to get wrong. The failure mode is not a bad address getting through: it is a real customer being blocked by a slow or inconclusive probe.

Rules that hold up in production

  • Verify on blur, not on every keystroke. One call per address, after the field loses focus.
  • Never block the submit button on the network. Give the call a short deadline of about two seconds; if it has not answered, accept the signup and verify asynchronously.
  • Block only invalid. Blocking risky or unknown rejects paying customers on catch-all corporate domains, which is most enterprise buyers.
  • Offer suggestion as a one-tap fix. Show "did you mean [email protected]?" with a button that fills the field. Never rewrite what a user typed.
  • Call from your backend. A key in front-end JavaScript is a public key.
  • Say why. "That address does not exist" is actionable; "invalid email" is not.
Signup form decisions
ResultForm behaviourCopy to show
validAccept silently.Nothing.
invalid, reason syntaxBlock, keep focus in the field."That does not look like an email address."
invalid, reason no_mxBlock, and show the suggestion if there is one."That domain cannot receive email. Did you mean …?"
invalid, reason mailbox_not_foundBlock."That mailbox does not exist at that domain."
risky, reason disposableYour policy. Block for paid plans, allow for trials."Please use a permanent email address."
risky, reason roleAccept, and flag for sales.Nothing.
risky, reason accept_allAccept.Nothing.
unknownAccept, re-verify in the background.Nothing.

Guides

Retries, timeouts and repeating safely

Every integration needs a retry policy, and most have one that makes outages worse. This is the policy the service is designed for.

retry.sh
# Retry 429 and 5xx only. Honour Retry-After when the server sends it.

Policy

Retry policy by response
ResponseActionWait
2xxDone.
429Retry, up to about five attempts.Retry-After, or exponential backoff with full jitter capped at 30 seconds.
500, 502, 503, 504Retry.Exponential backoff with jitter.
400, 401, 403, 404, 422Do not retry. Log and surface.
402Do not retry. Alert a human.
Network timeoutRetry once for a GET. For a POST /batches, reconcile first.Backoff.

Timeout budgets

  • POST /verify: at least 10 seconds. The SMTP probe holds a 5-second budget of its own, and a tighter client timeout manufactures false unknown results.
  • POST /batches: 10 seconds. The call only enqueues; it does not wait for verification.
  • Reads: 5 seconds is generous.

Repeating a write safely

The public API has no idempotency-key header, so a timed-out POST /batches is genuinely ambiguous: the batch may exist. Two habits remove the ambiguity.

  • Persist your own correlation id with the chunk before you call, and store the returned batch id against it. On a timeout, look up the correlation id rather than resubmitting blindly.
  • Make the consumer tolerant of duplicates. Keying results by address means a resubmitted chunk costs quota, not correctness.

RoadmapAn idempotency key on batch creation is a non-breaking addition and will appear in the changelog when it ships. Until then, treat batch creation as at-least-once.

Guides

The dashboard session API

The dashboard runs on a second, larger API at /api/v1. It is authenticated with a session cookie, requires a CSRF token on every write, and enforces roles and step-up MFA. It exists for the browser application, and it is documented here so that you do not build against it by mistake.

Not a machine interfaceSession routes are not part of the public contract. They change without a version bump, they require a CSRF token your server cannot obtain cleanly, and sensitive routes demand an MFA challenge that is minutes old. Automating them means an integration that breaks on a Tuesday for no visible reason.

Session API areas and their public-API equivalents
AreaSession routeUse this instead
Single verificationPOST /api/v1/verifyPOST /api/public/v1/verify
Bulk jobs/api/v1/verification-jobs/api/public/v1/batches
Key management/api/v1/api-keysThe dashboard. Key creation and revocation are deliberately human actions.
Team, roles, billing, security/api/v1/members, /roles, /billing, /security/*The dashboard, or SSO and SCIM for provisioning at scale.

Guides

Webhooks

v1 has no outbound webhooks. Batch completion is discovered by polling GET /batches/{id}, which the bulk guide shows in every language. Saying so plainly is more useful than a page describing an endpoint that does not answer yet.

What to build in the meantime

  • One poller per job, five seconds apart, with a ceiling. Stop when the status is terminal or when your own deadline passes.
  • Consume results incrementally. Rows are readable before the job reports completed, so a long job can feed a pipeline continuously.
  • Do not poll from a request handler. Enqueue the poll in a background worker; a user-facing request should never wait on a batch.

When webhooks arrive

Outbound webhooks are a gated feature in development. Adding a route and a signed callback is a non-breaking change under the versioning policy, so it will land inside v1, appear in the changelog, and require no migration from anyone who is polling. The one inbound webhook the platform already operates is the Stripe billing callback, which is signature-verified and not part of the public API.

Guides

MCP and agent access

Tenants can register Model Context Protocol servers in the dashboard, over stdio or HTTP, so an agent can reach verification through a tool call rather than a hand-written HTTP client.

  • Credentials are sealed. Bearer tokens and secrets attached to an MCP server are field-encrypted at rest and never returned by a read. You can replace one; you cannot retrieve it.
  • Give an agent its own API key, scoped to verify:read only. Agents are exactly the class of caller that should not hold verify:write.
  • Every call stays attributable. Usage is recorded against the key id, so an agent's spend is visible next to everything else.
  • Point the model at this page. The copy buttons above emit Markdown built from the live DOM, so what a model reads is what the page says, not a stale mirror of it.

Platform

Versioning and deprecation

For an API-first product this is a customer commitment, not an operational detail. It is published so you can plan against it.

Scheme

  • The major version lives in the path: /api/public/v1. There are no date-based versions, no header negotiation, and no v1.1.
  • A major version increments only for a breaking change. Everything else ships continuously inside v1 with no version signal.
  • At most two majors run at once, the current one and its predecessor.

What counts as breaking

Breaking versus non-breaking changes
Breaking, new major requiredNon-breaking, ships inside v1
Removing or renaming a field, route or enum valueAdding a field to a response
Changing a field's type or meaningAdding a value to an open enum such as reason
Tightening validation on existing inputRelaxing validation
Changing an error code stringAdding routes, optional parameters or headers
Changing authentication semanticsChanging a limit's value, with notice
Changing the meaning of a status or reason value, or adding a statusFixing a bug so behaviour matches the documentation

status is a closed enum and reason is open. That single distinction is what lets you switch exhaustively on one and never on the other.

Deprecation timeline

Deprecation timeline from written notice to sunset
WhenWhat happens
T+0Announcement by email to every tenant that called the surface in the previous 90 days, plus a changelog entry and a banner on this page.
T+0Responses start carrying Deprecation: true, Sunset: <HTTP-date> (RFC 8594) and Link: <guide>; rel="deprecation".
T+12 monthsEarliest permitted sunset for a whole major version. A single field or route inside a live version: no earlier than 6 months.
90, 30 and 7 days beforeReminders, sent only to tenants still calling the surface.
SunsetThe surface returns 410 with code endpoint_sunset and the migration-guide URL. Never a silent removal, and never a 404.

An announced sunset date can be extended and never shortened. Enterprise agreements may negotiate longer windows, never shorter ones.

Platform

Security and data handling

Verification means handling other people's contact data, so the handling rules matter as much as the endpoint list.

Security and data-handling properties
PropertyPosition
TransportHTTPS only, modern TLS, with a post-quantum migration path set out in the platform architecture.
CredentialsAPI keys are stored as SHA-256 hashes and matched in constant time. Passwords use Argon2id. Secrets attached to integrations are field-encrypted.
Tenant isolationRow-level security in the database, enforced per request, and covered by an isolation test suite. A resource belonging to another tenant answers 404, not 403.
Card dataNever touches our servers. Payment details live with Stripe.
Verified addressesKept only as long as your retention setting allows, then erased. Crypto-shredding backs the erasure guarantee.
AuditKey use, key creation and revocation, and privileged actions are recorded with actor, IP and key prefix.
RegulatoryBuilt against GDPR, Québec's Law 25, PIPEDA and CCPA. See compliance and privacy.
AccessibilityEvery surface, this page included, targets WCAG 2.2 AAA contrast and full keyboard operation. See accessibility.

Your side of the line

  • Verify only addresses you have a lawful reason to hold. A verification service is not a discovery service, and bought lists stay bought lists.
  • Do not log full addresses at debug level. Log the verdict and a hash if you need to correlate.
  • Keep keys out of client applications and out of source control.
  • Report anything you find to the contact in security.txt. See the security page for scope.

Platform

Changelog

Every change to the public contract lands here first. Non-breaking additions ship inside v1 continuously; anything breaking follows the deprecation timeline.

2026-08-20 · Documentation
This reference published: full v1 surface, the decision table, the error registry, and runnable examples in fourteen languages. Section and whole-page Markdown export added for model consumption.
2026-08-20 · Policy
Versioning and deprecation policy published, including the Deprecation, Sunset and Link response headers and the endpoint_sunset error code.
2026-08-13 · Tools
MX, SPF, DKIM and DMARC checkers extended with transport-security records. Free, no key required.
Q3 2026 · API
v1 public API in early access. Request a key.

Platform

Glossary

Accept-all (catch-all) domain
A domain configured to accept mail for every local part. An SMTP probe cannot distinguish a real mailbox from a nonexistent one there, which is why the honest verdict is risky with reason accept_all.
Batch
An asynchronous verification job created by POST /batches, holding up to 500 addresses and identified by a UUID.
Cursor
An opaque token identifying a position in a result set. Passed back unchanged as cursor; null in next_cursor means the walk is finished.
Greylisting
A receiving server deliberately deferring a first delivery attempt. It produces unknown, not invalid.
Idempotent
Safe to repeat with the same effect. Every GET here is idempotent; POST /batches is not, so it needs a correlation id on your side.
Open enum
A field that may gain values without a major version bump. reason is open; status is closed.
Quota
The verifications your plan includes per billing period. Exhaustion returns 402 quota_exceeded.
Rate limit
A ceiling on request frequency per key. Exhaustion returns 429 rate_limited with Retry-After.
Role account
A shared function mailbox such as info@, sales@ or abuse@. Deliverable, and rarely a person.
Scope
A capability attached to an API key at creation: verify:read, verify:write or jobs:read. A missing scope returns 403 insufficient_scope.
SMTP probe
A conversation with the receiving server that stops at the RCPT stage. No message is ever sent.
Step-up authentication
A fresh MFA challenge required for sensitive dashboard actions, including revoking a key. It has a five-minute window.

Platform

Frequently asked questions

Which endpoint should a signup form call?

POST /verify, from your backend, on blur, with a short deadline and a fallback that accepts the signup if the call is slow. See validating a signup form.

Why is an address I know is real coming back unknown?

Because the SMTP probe did not complete. Greylisting, aggressive filtering and connection blocking all produce this, and none of them say anything bad about the address. Re-verify later, and treat unknown as inconclusive rather than negative.

Can I get a definitive answer on a catch-all domain?

No, and neither can anyone else. The domain accepts every recipient by design, so the protocol itself has no way to distinguish a real mailbox. Use engagement history for those contacts instead.

Do batch results expire?

They are retained for as long as your tenant's retention setting allows, then erased. Export what you need into your own system rather than treating the API as long-term storage.

Is there a client library for my language?

Not yet. The examples on this page are deliberately dependency-free so that they work today in any stack, and each one is short enough to paste into a service and own. Official SDKs are on the roadmap.

What happens to the addresses I send you?

They are verified, stored under your tenant with row-level isolation for the length of your retention window, and then erased. They are never sold, shared, or used to build a list. See privacy.

How do I test without spending quota?

Use a key that begins mp_test_. Same routes, same shape, no quota consumption.

Why is there no page parameter?

Offset pagination gets slower as the offset grows and skips or repeats rows when data changes underneath it. Cursors have neither problem. Follow next_cursor until it is null.

Ready for a key?

The v1 API is in early access. Tell us what you are building and we will send keys with your onboarding.