Verify at the signup form
The cheapest bounce to prevent is the one that never enters your database. A typo caught while somebody is still on the page costs you nothing to fix.
// On blur, not on every keystroke: one check per address, not one per letter.
// This runs on your server. The address travels in the body, never in a URL.
const res = await fetch("https://api.zapbounce.com/v1/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZAPBOUNCE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email }),
});
const { result, reason, disposable, did_you_mean } = await res.json();
if (result === "invalid") showError(messageFor(reason, did_you_mean)); // block, and say why
if (disposable) showError("Please use a permanent address."); // a flag, not a result
// valid, catch_all and unknown all continue: never block on an unproven resultWhat to do with each verdict
The mistake is treating every non-valid result as a reason to block someone.
| result | At a signup form |
|---|---|
| valid | Accept it. |
| invalid | Block and explain. This is a typo or a dead mailbox, and the person can fix it while they are still there. If did_you_mean has a suggestion, offer it. |
| catch_all | Accept it. The domain accepts everything, so nothing was disproven. |
| unknown | Accept it. The server would not answer, which is not the person's fault. |
Blocking on catch-all or unknown turns a server's evasiveness into a lost customer.
| Flag | At a signup form |
|---|---|
| disposable: true | Block it if your product is worth protecting. Say why, because people usually try again with a real address. |
| role: true | Accept it. info@ is the right address for plenty of businesses. |
| free_provider: true | Accept it. You might route it differently if you sell to companies, and that is your call to make. |
Flags are booleans that sit beside the result, so a disposable address can be valid and a role address can be catch-all. Check them separately.
Getting it right in the form
Check on blur, not on keystroke
One check when the field loses focus. Checking per keystroke burns credits and rate limits, and the answer cannot be right until they finish typing.
Keep the key on your server
Call the API from your backend. A key in client-side JavaScript is a key anyone can read and spend.
Fail open, always
If the check times out, let the signup through. An outage on our side should never cost you a customer.
Say what is wrong, not that it is wrong
'That address bounced when we checked it' is actionable. 'Invalid email' makes people retype the same thing.
Where this does not help
Questions
How fast is a single check?
A DNS lookup and a short SMTP conversation. Set a timeout of a second or two and fail open past it, because a signup form should never wait on someone else's mail server.
Should I block disposable addresses?
If the thing behind the form has a cost, yes. If you are collecting newsletter signups, blocking them mostly annoys people who would not have engaged anyway.
Does this replace double opt-in?
No. Verification says an address can receive mail. Double opt-in proves someone wanted it, which is the part that protects your sending reputation.
What about rate limits?
Documented per plan. A form does not usually come close, but a bulk import running through the single endpoint will.
Wire it into your form
100 free checks a month covers most signup volumes while you test.