Rules
Rules
Choose what each check does on your app, and whether a refused person gets a link to a page that explains why.
Check settings per app
On the Rules tab of your dashboard, the Checks section lists each check with our default. For each check, you pick what it does on that app. We set the points.
- Default uses our points and rules, as the reason codes describe.
- Off gives the check 0 points. It still shows in the check log and in the API's reasons, so you can see what it found.
- Always challenge makes the signup at least a challenge when the check fires. Its points still count in the score, but never toward a block. A block needs your other reasons to reach the block score on their own.
- Always block makes the signup a block when the check fires, even for a weak signal such as a role address.
Your overrides come first. An allow override still allows a signup whose check is set to always block. Invalid addresses and domains without a mail server always block, because mail to them can't arrive. The velocity reasons have their own thresholds instead.
A setting applies only to checks that count on your plan. The other checks show the plan they need, and you can set them after you upgrade.
Only the strongest network reason counts. If you turn the strongest one off, the next strongest network reason counts. On a hosting IP that is also a Tor exit, turning Tor off leaves the hosting network's 40 points.
On an address in an AI agent's published IP range, the hosting network check doesn't count. To challenge agent signups, set the AI agent check to Always challenge. If you set the hosting network check to Always challenge or Always block, it still applies on agent addresses.
When a setting applies, the check log says so on the reason's line, such as "off in your settings". In API answers, that reason carries setting with the value off, challenge or block.
With two or more apps, you can copy an app's Rules settings to another app on the Rules tab. The other app keeps its mode. We copy the overrides only if you tick the box for them in the confirm window.
A "Why was this refused?" link
You can give a refused person a link to our page about their email's domain, such as /why/mailinator.com. The page says in plain words what we know about the domain and how to sign up another way. The page never shows your settings, your score cutoffs or your checks. The link carries the domain only, never the full address.
The link is off by default, because the refusal message is part of your app. To turn it on, open Advanced on the Rules tab of your dashboard, switch on "Add a 'Why was this refused?' link" and choose Save hook settings.
- The Supabase hook then adds "Why was this refused?" and the link to your message. Supabase passes the message to your app as the sign-up error.
- Show
why_urlfrom/v1/checkonly when you refuse the signup, afterblock_message. - The helpers under Code for your stack pass the link on. In TypeScript, call
signupRefusalWithLinkinstead ofsignupRefusal. In Python, callsignup_refusal_with_link. Both returnnull(Nonein Python) when the signup may go ahead, which includes a failed check. When you should refuse the signup, they return the message and the link. The link isnull(Nonein Python) while the setting is off. The Next.js route handler and the Express and Hono examples return it aswhy_urlnext to the error. The Next.js server action returns it aswhyUrlin its state. The Better Auth plugin putswhy_urlin the error body next to the message.authClient.signUp.email()and sign-in with a Google ID token return it on the error. The plugin's OAuth and magic-link redirects carry only the message. The Firebase function adds the link to the end of its message.
// After POST /v1/check, when you refuse the signup:
if (check.action === "block") {
return {
error: check.block_message ?? "Please sign up with a permanent email address.",
whyUrl: validWhyUrl(check.why_url), // null unless the link is on for this app
};
}
// The check our helpers use: a well-formed http(s) link with a host, returned unchanged, or null.
function validWhyUrl(u) {
if (typeof u !== "string" || u.length > 2048 || !/^https?:\/\/[^\s/\\?#][^\s\\]*$/i.test(u)) return null;
try {
const p = new URL(u);
return (p.protocol === "https:" || p.protocol === "http:") && p.hostname !== "" ? u : null;
} catch {
return null;
}
}
// Where your form shows the error (JSX):
<p role="alert">
{error} {whyUrl && <a href={whyUrl}>Why was this refused?</a>}
</p>Anyone can also open /why and type an address. Link /why from your help pages if you like. Whether the link has rel="nofollow" is your choice.