Address validation

POST /email/validate says whether an address is likely to receive: syntax, mail route, disposable, role account, typo, and your own suppression list. One address, a batch of a hundred, or a background job of fifty thousand.

Validate an address

Synchronous, because you call it from your own signup form and need the answer while the person is still typing. It needs a token with the email:validate scope.

cURL
curl -X POST https://api.mailer.quathos.com/api/v1/email/validate \
  -H "Authorization: Bearer $QUATHOS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "contato@gmial.com" }'
json
{
  "email": "contato@gmial.com",
  "domain": "gmial.com",
  "verdict": "risky",
  "score": 45,
  "checks": {
    "valid_syntax": true,
    "has_mail_exchange": true,
    "disposable": false,
    "role_account": true,
    "suppressed": false
  },
  "did_you_mean": "contato@gmail.com",
  "reason": "role_account"
}
A role account at a domain one edit away from gmail.com.

A list in one call

POST /email/validate/batch takes up to 100 addresses and answers in the same request, one result per address, in the shape of the single call. Duplicates are dropped first — the same address twice in one list is one validation, not two — so results can be shorter than what you sent, and it keeps the order in which each address first appeared. The response also carries usage, and every reply carries X-Validation-Quota-Remaining.

cURL
curl -X POST https://api.mailer.quathos.com/api/v1/email/validate/batch \
  -H "Authorization: Bearer $QUATHOS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["ana@example.com", "info@example.com", "ana@example.com"] }'

{
  "results": [
    { "email": "ana@example.com",  "verdict": "deliverable", "score": 100, "reason": "ok" },
    { "email": "info@example.com", "verdict": "risky", "score": 60, "reason": "role_account" }
  ],
  "usage": { "used": 314, "quota": 5000, "remaining": 4686 }
}
The allowance for the whole list is checked BEFORE anything is validated: if 3,000 are left and the batch asks for 5,000, the call comes back with quota_exceeded and the number that is left, and not one address is touched. Half a list validated and charged, with the rest refused mid-way, would make the answer a lie about what happened.

The mail route is resolved once per DOMAIN, not once per address, so a list where half the addresses are at the same provider costs half the DNS. Above 100 addresses the call is refused with a message pointing at the job endpoint below — the answer would not fit in the time any HTTP client is willing to wait.

Lists too big for one request

POST /email/validate/jobs accepts up to 50,000 addresses and answers immediately with a job id. The work runs in the background, in slices, and the counters advance as it goes — so a long job is something you watch, not something you wait on. It takes lists of any size, including small ones: if you integrate through a queue, an id is easier than a wait.

cURL
curl -X POST https://api.mailer.quathos.com/api/v1/email/validate/jobs \
  -H "Authorization: Bearer $QUATHOS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "emails": ["ana@example.com", "...", "zoe@example.com"] }'

{ "id": "01a07f...", "status": "queued", "total": 42310, "processed": 0 }

GET /email/validate/jobs/{id} returns the status (queued, running, done, failed), the counters, and — once done — result_url: a signed link to a CSV with one row per address, in the order you sent them, with the verdict, the score, the reason and every individual check. The link lasts an hour and the file itself is kept for 7 days; ask for the job again to get a fresh link while the file is still there. GET /email/validate/jobs lists your jobs, most recent first, with the same cursor as every other listing.

cURL
curl https://api.mailer.quathos.com/api/v1/email/validate/jobs/$JOB_ID \
  -H "Authorization: Bearer $QUATHOS_TOKEN"

{
  "id": "01a07f...",
  "status": "done",
  "total": 42310,
  "processed": 42310,
  "valid_count": 36104,
  "risky_count": 2571,
  "invalid_count": 3552,
  "unknown_count": 83,
  "result_url": "https://.../validation-jobs/....csv?X-Amz-Signature=..."
}

Each address in a job counts one validation, and the allowance for the whole list is anticipated when the job is created, exactly like the synchronous batch: a job that does not fit is refused at creation, not abandoned halfway. A job interrupted by a restart resumes where it stopped instead of validating anything a second time, and a job that fails says so in error_message — without ever echoing an address back at you.

The four verdicts

VerdictWhat it means
deliverableValid syntax, the domain accepts mail, and no risk signal.
undeliverableInvalid syntax, no MX or A record, or already suppressed for you.
riskyIt should receive, but something is off: disposable domain, role account, or a likely typo.
unknownWe could not determine it: a DNS timeout. Neither a pass nor a fail.

checks carries the individual signals so you can write your own policy instead of inheriting ours. has_mail_exchange is deliberately nullable: null means the DNS lookup did not answer, which is not the same as the domain having no mail route.

What it does not do

It does not open an SMTP connection to the destination to ask whether the mailbox exists. Probing mailboxes from a sending IP is one of the behaviours receivers classify as spammer activity, and it would burn the reputation the rest of the platform spends months building.

So the ceiling of certainty here is unknown, never a confident "this mailbox exists". Use it to catch typos, disposables and dead domains before they become bounces, not as proof that someone is on the other end.

It does look at YOUR suppression list. An address that hard bounced or unsubscribed for you comes back undeliverable even when the domain is perfectly healthy, because that is the address we will refuse to send to anyway.

Quota

Each live validation counts against the monthly allowance of your plan; test-mode tokens are exempt and never count. Past the allowance the call is refused rather than charged: this version does not bill the wallet for overage.

cURL
curl https://api.mailer.quathos.com/api/v1/email/validate/usage \
  -H "Authorization: Bearer $QUATHOS_TOKEN"

{ "used": 312, "quota": 5000, "remaining": 4688 }

Every validate response also carries X-Validation-Quota-Remaining, so you can watch the budget without a second call.