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.

Shell
// 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 result

What to do with each verdict

The mistake is treating every non-valid result as a reason to block someone.

resultAt a signup form
validAccept it.
invalidBlock 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_allAccept it. The domain accepts everything, so nothing was disproven.
unknownAccept 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.

FlagAt a signup form
disposable: trueBlock it if your product is worth protecting. Say why, because people usually try again with a real address.
role: trueAccept it. info@ is the right address for plenty of businesses.
free_provider: trueAccept 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

  1. 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.

  2. 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.

  3. Fail open, always

    If the check times out, let the signup through. An outage on our side should never cost you a customer.

  4. 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.