Result codes

The status and reasonCode fields

Four statuses, and the interesting one is last: valid, invalid, catch-all, unknown. The status field answers one question, whether this mailbox exists, and those are the only four answers SMTP can honestly give. On the wire the third is spelled catch_all, with an underscore.

reasonCode is the narrower cause behind the status. Each reason belongs to exactly one status, so you can branch on status and treat the reason as detail.

Role, disposable and free provider are not statuses. They are flags carried in the flags array, because they describe the address and say nothing about the server's answer. A role address can be valid or invalid, and a disposable domain can be catch-all.

Unknown is a first-class status. It means the server would not give a straight answer: greylisting, throttling, or a block on probe traffic. We report it, we explain it with a reason code, and the credit is refunded.

Fields

FieldTypeWhat it carries
statusstringOne of valid, invalid, catch_all, unknown. Branch your pipeline on this.
reasonCodestringThe narrower cause, from the table above.
smtpCodeinteger or nullThe literal reply code from the server. Null when the conversation never reached RCPT TO.
flagsarrayAny of role, disposable, free_provider. Never values of status.

Reference

statusSMTP behavior behind itCredit
validRCPT TO answered 250 and the domain is not catch-all.Spent
invalidA definite no: no such domain, no mail server, a bad address, or a 5xx for this recipient.Spent
catch_allThe domain accepts every address, so a mailbox cannot be proven to exist.Spent
unknown4xx, a timeout, a block on the probe, or a connection that failed.Refunded

Catch-all is a result, not a warning. It tells you something true about the domain, which is why it is billed and unknown is not.

Flags

FlagPresent when
roleThe local part is a function and not a person: info@, sales@, support@.
disposableThe domain belongs to a throwaway-mailbox provider.
free_providerThe domain is a consumer mailbox provider such as gmail.com or yahoo.com.

Flags come from the address itself and need no answer from the server, so they are set on every result, unknown included.

Reason codes

reasonCodestatusWhat happened
MAILBOX_EXISTSvalidThe server accepted this recipient and the domain is not catch-all.
MAILBOX_NOT_FOUNDinvalidThe server said there is no such user.
BAD_SYNTAXinvalidThe string is not shaped like an email address.
NO_MAIL_SERVERinvalidA null MX, or a domain with neither an MX nor an A record.
CATCH_ALLcatch_allThe server accepts any address at this domain.
SENDER_BLOCKEDunknownThe server or a blocklist refused our sending address.
TEMPORARY_FAILUREunknownA 4xx such as greylisting or throttling, or a DNS lookup that failed.
TIMEOUTunknownThe server did not answer in time.
CONNECTION_FAILEDunknownNo mail server accepted a connection, or it dropped mid-conversation.
DUPLICATEunknownThe same address appeared earlier in the request. The first occurrence carries the real result.
NO_CAPABLE_NODEunknownNo verification node could take this address's provider, so it was closed out as unknown and its credit refunded.

This list can grow. Branch on status, and treat a reason you do not recognize as extra detail.

What your code should do with unknown

The answer depends on who is waiting. At a signup form, accept the address and queue a recheck for later that night. In a bulk clean, put unknowns in a retry pile and resubmit them the next day. They were refunded, so a second try only keeps a credit if it gets a verdict. After two tries, move whatever is left into a segment you send separately and watch closely.

In a CRM, the trap is the data model. A boolean column called is_valid has two values, and unknown needs a third. Somebody will map it to false one day, and a few thousand real contacts will get suppressed without anyone deciding that they should be. Store the verdict as the string you received, with checkedAt beside it.

Test this branch deliberately before you need it. In production it fires only when a mail host is having a bad day. That's a poor time to learn that your pipeline crashes on it. A stub of your own that returns unknown with TEMPORARY_FAILURE is enough.

Auditing a verdict you don't believe

Say a customer emailed you yesterday and today their address comes back invalid. Look at smtpCode first. A 550 means the server said no such user at the moment we asked. Compare the stored address with the one they wrote from, character by character, because a typo in your CRM is the usual cause.

For an unknown, the code tells you whether a retry is worth it. A 4xx is temporary, so try again tomorrow. With a policy block such as SENDER_BLOCKED, you're looking at the server's choice, and asking again won't change its mind.

When a valid address bounces, check the age of the verdict in checkedAt before you blame the check. People change jobs, and a verdict from four months ago describes a mailbox as it was back then.

None of this can confirm that a catch-all address is real. The raw code for those is a 250, same as a good mailbox, and that's exactly why the label exists.

Questions developers ask

Should I mail catch-all addresses?

Send to them separately from your valid segment and watch the bounce rate on that send alone. Then you are deciding on your own data instead of a vendor's guess.

Why is unknown refunded?

Because you would be paying for a non-answer. Charging for it would also reward a vendor for keeping the unknown pile large.

Can a role address be worth keeping?

Often, for support and billing contact. It is flagged because shared mailboxes have poor engagement and some senders treat them as a list-quality signal. The flag sits beside the status, so you still know whether the mailbox exists.

Can any verifier remove spam traps?

Treat that promise carefully. A pristine trap looks exactly like a real mailbox over SMTP. What verification does well is remove addresses that would bounce and separate the catch-alls.

Try it against a sandbox key

Scripted verdicts, no SMTP connections, no credits. Live keys come with 100 free checks a month and no card.