Four results, and the interesting one is last. Valid, invalid, catch-all, unknown. The result 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, like every other value in this API.
Role, disposable and free provider are not results. They're booleans that sit beside result, 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. Fold them into one field and you lose that. You'd also need a precedence rule nobody remembers.
Each result traces back to something the receiving server said. The smtp_code field carries that raw response, so you can audit how we read it rather than taking our word for the mapping.
Unknown is a first-class result here. It means the server would not give a straight answer: greylisting, throttling, or an outright block on probe traffic from cloud addresses. We report it, we explain it, and we do not charge for it.
Fields
| Field | Type | What it carries |
|---|---|---|
| result | string | One of valid, invalid, catch_all, unknown. Branch your pipeline on this. |
| reason | string | The narrower cause, such as mailbox_not_found or greylisted. Every value is in the reasons table below. |
| smtp_code | string or null | The literal server response. Null when the handshake never got as far as RCPT TO. |
| role, disposable, free_provider | boolean | Flags about the address. Never values of result. |
| billed | boolean | True on valid, invalid and catch_all. False on every unknown and every collapsed duplicate. |
Reference
| result | SMTP behavior behind it | Billed |
|---|---|---|
| valid | RCPT TO answered 250 and the domain is not accept-all. | Yes |
| invalid | A definite no: no such domain, no mail server, or a 5xx for this recipient. (A row rejected on syntax alone is invalid too, and is the one invalid that is free.) | Yes |
| catch_all | 250 for the real address and 250 for a random one at the same domain. | Yes |
| unknown | 4xx, 252, 530, a timeout, or a block on the probe. | No |
Catch-all is a result, not a warning. It took a full handshake and a second probe, and it tells you something true about the domain, which is why it costs a credit and unknown does not.
Flags
| Flag | True when | How we know |
|---|---|---|
| role | The local part is a function and not a person: info@, sales@, support@. | List lookup. |
| disposable | The domain belongs to a throwaway-mailbox provider. | List lookup. |
| free_provider | The domain is a consumer mailbox provider such as gmail.com, yahoo.com or outlook.com. | List lookup. |
Flags are set on every response, unknown included. They come from the address itself and need no answer from the server, and they never change what you are billed.
Reasons
| reason | result | What happened |
|---|---|---|
| accepted_email | valid | The server accepted this recipient and rejected a random one. |
| invalid_syntax | invalid | The string is not shaped like an email address. Seen in batch and file results only, and never billed: POST /verify rejects the same input as a 400 malformed_email. |
| domain_not_found | invalid | The domain does not exist in DNS. |
| no_mail_server | invalid | No MX record, and no A record to fall back on. |
| null_mx | invalid | The domain publishes a null MX, which says it accepts no mail at all (RFC 7505). |
| mailbox_not_found | invalid | The server said no such user. |
| mailbox_disabled | invalid | The mailbox exists and has been closed or suspended. |
| mailbox_full | invalid | Over quota. This is the one invalid that can recover by itself, so check it again after 30 days. |
| accept_all_domain | catch_all | The server accepted a random address too. |
| greylisted | unknown | A 4xx asking us to come back later, and the retry did not settle it. |
| throttled | unknown | The host rate-limited our probes. |
| probe_blocked | unknown | The host refuses to discuss recipients with us, often a 530. |
| timeout | unknown | No reply inside your timeout budget. |
| temporary_failure | unknown | Another 4xx that says nothing about the mailbox. |
| cannot_verify | unknown | A 252: the server will accept the mail and won't confirm the address. |
This list is closed today and can grow without a version bump. Branch on result, and treat a reason you don't 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 weren't billed, so a second try costs nothing unless 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 checked_at beside it.
Test this branch with the sandbox 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.
Auditing a verdict you don't believe
Say a customer emailed you yesterday and today their address comes back invalid. Look at smtp_code 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 252 or a 530, you're looking at the server's policy, and asking again won't change its mind.
When a valid address bounces, check the age of the verdict in checked_at 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 not billed?
Because you would be paying for a non-answer. Charging for it also gives the vendor a reason to keep the unknown pile large, which is the wrong incentive to build into a price list.
Can a role address be worth keeping?
Often, for support and billing contact. It is flagged because open rates on shared mailboxes are poor and some ESPs treat them as a list-quality signal. The flag sits beside the result, so you still know whether the mailbox exists.
Is a catch-all result billed?
Yes, one credit. We ran the full handshake and a second probe, and the answer tells you something true about the domain. Unknown is the one result that is free, because there we found nothing out.
Do you remove spam traps?
No, and treat anyone who promises it carefully. A pristine trap looks exactly like a real mailbox over SMTP. We may flag a likely recycled trap by heuristic, labeled as a guess.