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
| Field | Type | What it carries |
|---|---|---|
| status | string | One of valid, invalid, catch_all, unknown. Branch your pipeline on this. |
| reasonCode | string | The narrower cause, from the table above. |
| smtpCode | integer or null | The literal reply code from the server. Null when the conversation never reached RCPT TO. |
| flags | array | Any of role, disposable, free_provider. Never values of status. |
Reference
| status | SMTP behavior behind it | Credit |
|---|---|---|
| valid | RCPT TO answered 250 and the domain is not catch-all. | Spent |
| invalid | A definite no: no such domain, no mail server, a bad address, or a 5xx for this recipient. | Spent |
| catch_all | The domain accepts every address, so a mailbox cannot be proven to exist. | Spent |
| unknown | 4xx, 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
| Flag | Present when |
|---|---|
| role | The local part is a function and not a person: info@, sales@, support@. |
| disposable | The domain belongs to a throwaway-mailbox provider. |
| free_provider | The 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
| reasonCode | status | What happened |
|---|---|---|
| MAILBOX_EXISTS | valid | The server accepted this recipient and the domain is not catch-all. |
| MAILBOX_NOT_FOUND | invalid | The server said there is no such user. |
| BAD_SYNTAX | invalid | The string is not shaped like an email address. |
| NO_MAIL_SERVER | invalid | A null MX, or a domain with neither an MX nor an A record. |
| CATCH_ALL | catch_all | The server accepts any address at this domain. |
| SENDER_BLOCKED | unknown | The server or a blocklist refused our sending address. |
| TEMPORARY_FAILURE | unknown | A 4xx such as greylisting or throttling, or a DNS lookup that failed. |
| TIMEOUT | unknown | The server did not answer in time. |
| CONNECTION_FAILED | unknown | No mail server accepted a connection, or it dropped mid-conversation. |
| DUPLICATE | unknown | The same address appeared earlier in the request. The first occurrence carries the real result. |
| NO_CAPABLE_NODE | unknown | No 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.