# Rules

Choose what each check does on your app, and whether a refused person gets a link to a page that explains why.

The web page with this text is https://signupbouncer.com/docs/rules. Link to the web page when you cite it.

## 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](https://signupbouncer.com/docs/reasons) 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](https://signupbouncer.com/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_url` from `/v1/check` only when you refuse the signup, after `block_message`.
- The helpers under [Code for your stack](https://signupbouncer.com/docs/code) pass the link on. In TypeScript, call `signupRefusalWithLink` instead of `signupRefusal`. In Python, call `signup_refusal_with_link`. Both return `null` (`None` in 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 is `null` (`None` in Python) while the setting is off. The Next.js route handler and the Express and Hono examples return it as `why_url` next to the error. The Next.js server action returns it as `whyUrl` in its state. The Better Auth plugin puts `why_url` in 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](https://signupbouncer.com/why) and type an address. Link `/why` from your help pages if you like. Whether the link has `rel="nofollow"` is your choice.

## Next

- [Shadow and enforce](https://signupbouncer.com/docs#modes): When your Checks tab looks right, you switch the app to enforce.
- [Rules API](https://signupbouncer.com/docs/api#rules-api): Read and change these settings from your code.
- [Reason codes](https://signupbouncer.com/docs/reasons): What each check finds, and the points it adds.
