Skip to content

Gate a signup form with the sending policy

published · September 13, 2026 · updated · September 23, 2026

One boolean per answer, decided by rules you set once: which signals block, how high the score may go, how sure a catch-all must be. How to use ok_to_send.

A sending policy is the set of rules an account keeps about which email answers it will act on. Every verification ends with ok_to_send, the boolean those rules produce for that answer, and blocked_by, the name of the rule that decided a refusal. The score stays arithmetic and the verdict stays a verdict; the policy is where a decision is made, and it is made once, on the server, so a signup form, a batch job and a bulk run all branch the same way.

Why a boolean, when the answer has 46 fields

Because the fields are facts and a form needs a decision. risky is a verdict that covers a burner and a catch-all at a well-run company, and those deserve opposite treatment on a signup form. A team that reads the fields ends up with an if-chain that grows with every new signal and drifts between the form, the import script and the campaign filter. A policy puts the chain in one place, applies it to every channel, and leaves blocked_by in the answer so the reasoning is visible.

Three rules make up a policy, all optional.

Rule What it does
block_signals A list of risk-signal names. Any answer whose risk_signals carries one of them is blocked, whatever its score.
max_risk_score A ceiling. An answer whose risk_score is above it is blocked.
min_mailbox_confidence A floor for a mailbox the handshake could not confirm. An answer with a mailbox_confidence below it is blocked; a confirmed mailbox has no confidence figure and never fails this rule.

They are applied in a fixed order after the verdict: an undeliverable verdict is blocked first and always, then the first blocked signal in the published weights' order, then the score ceiling, then the confidence floor. blocked_by names the first rule that fired: undeliverable, the signal's own name, risk_score or mailbox_confidence.

The defaults

Until an account sets a policy, the published defaults apply. They block the five signals that are almost certainly wrong, disposable, likely_typo, parked_domain, mx_blocklisted and no_reply, and any score over 60, and they set no confidence floor. A role address at 30, a plus-tag at 5, a relay alias at 5 and a catch-all at 30 all pass by default; a gibberish username on a catch-all domain with no SPF, at 60, passes too, and at 70 with a plus-tag it would not.

The defaults are what the guest demo and the sandbox key answer with, because neither has an account to keep a policy: there [email protected] answers ok_to_send: false and blocked_by: "disposable". The test addresses answer under the account's stored policy, like every other lookup a key makes, which makes them a way to try a policy before a customer meets it: [email protected] answers ok_to_send: true under the defaults and blocked_by: "role" on an account that blocks role.

{
  "email": "[email protected]",
  "deliverable": "risky",
  "reason": "disposable",
  "risk_score": 90,
  "ok_to_send": false,
  "blocked_by": "disposable"
}

Setting a policy

The policy belongs to the account and applies to every channel the account is billed for: the single and batch endpoints, bulk runs, monitors, the form widget and the dashboard. Read it with GET /api/v1/email/policy, change it with PUT, and restore the defaults with DELETE. All three are free.

curl -X PUT https://spaw.co/api/v1/email/policy \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"max_risk_score": 40, "block_signals": ["disposable", "likely_typo", "parked_domain", "mx_blocklisted", "no_reply", "recycled_mailbox"], "min_mailbox_confidence": 60}'

Send only the keys you are changing. A null clears the ceiling or the floor, an empty list blocks on no signal, and a body naming none of the three rules answers 422, because that is the shape a misspelled key takes. The answer carries the policy in force, is_default, and when it was last changed. The Sending policy tab on the verification page edits the same three rules with checkboxes.

What a signup form should block

A signup form is the strictest gate most products need, and also the one where a false refusal costs a customer. Three policies cover most cases.

A consumer product that mails receipts and product updates. Keep the defaults. They stop the addresses that would never receive anything and let everything human through, including role inboxes and relay aliases, which are real people's mail.

A B2B trial with a free credit. Add role to the blocked signals, since info@ cannot start a trial, and lower the ceiling to 40 so a catch-all at a young domain with no SPF is held for a manual look rather than granted the credit. Keep relays allowed; a relay user is a person.

A newsletter or a lead list. Set a confidence floor of 60 so a catch-all address is only accepted when the domain looks administered and the username looks like a person, and add recycled_mailbox to the blocked signals so a mailbox that came back from the dead is held until the person re-engages.

None of these should block unverified. A handshake that timed out is a fact about the mail server's mood, not about the person, and a form that refuses an address because a server was greylisting will refuse real customers at random. Let the address in and, if the mailbox matters, verify it again later or send the confirmation mail and let the outcome decide.

Using it in the form

The form widget verifies as the visitor types, with a publishable key and, if you turn it on, Cloudflare Turnstile in front of the lookup so bots cannot spend your credits. It never blocks a submit on its own; it fires a spaw:result event whose detail is the full answer, and the decision is one listener away.

<script src="https://spaw.co/spaw-form.js" data-key="pk_live_…" defer></script>
<input type="email" name="email" data-spaw-email>
<button type="submit" id="signup">Create account</button>
<script>
addEventListener('spaw:result', (event) => {
    if (event.detail.type !== 'email') return;
    const button = document.getElementById('signup');
    button.disabled = event.detail.ok_to_send === false;
    if (event.detail.blocked_by === 'disposable') {
        // show "please use an address you will still read next month"
    }
});
</script>

The browser endpoint answers the same fields under the account's policy, so the widget and your server agree without any duplicated logic. A server-side check on submit, with the secret key, is still worth doing, because a browser can be told to ignore its own JavaScript; the answer will be the same, and repeats inside seven days are free.

Say why. blocked_by exists so the form can tell the person what to change: a typo-squat gets the did_you_mean suggestion, a burner gets a request for a lasting address, a no-reply address gets a request for one that is read. A refusal with no reason reads as a broken form.

What the policy does not do

It does not change the verdict, the score or any field; it adds two. It does not block a relay alias unless you tell it to, and this guide's advice is that you should not. It cannot let an undeliverable answer through, because there is no mailbox to send to. And it does not know your product: a policy is a floor under your judgement, not a replacement for the segment-level decisions the fields support, such as keeping role addresses out of drip campaigns while accepting them at the door.

What to do next

Related

markdown version: /guides/gate-a-signup-form-with-the-sending-policy.md

Verify addresses the same way

The Spaw API runs every check described here on each lookup, with an SMTP handshake that never sends mail. 10 free lookups a month, no card required.

Get your API key

More guides