# Gate a signup form with the sending policy

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.

Date: 2026-09-13

Updated: 2026-09-23

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 `disposable@spaw.test` 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: `role@spaw.test` answers `ok_to_send: true` under the defaults and `blocked_by: "role"` on an account that blocks `role`.

```json
{
  "email": "signup2847@mailinator.com",
  "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.

```bash
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.

```html
<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

- Read the sending policy section of the [email verification docs](/docs/email-verification) for the contract and the decision order.
- Read yours with [`GET /api/v1/email/policy`](/docs/api/get-email-policy) and change it with [`PUT`](/docs/api/update-email-policy).
- Put the widget on the form through the [browser endpoint](/docs/api/verify-email-public) with a publishable key.
- See what each weight means in [how Spaw computes the risk score](/guides/how-spaw-computes-the-risk-score).

Reference: https://spaw.co/guides/gate-a-signup-form-with-the-sending-policy
