# Gate SMS verification codes with the SMS policy

One boolean per phone answer, decided by rules you set once: the countries you send to, the line types and signals you refuse. How to use ok_to_send.

Date: 2026-09-24

Updated: 2026-09-27

An SMS policy is the set of rules an account keeps about which phone numbers it will send a code or a message to. Every phone lookup 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 flags stay facts; the policy is where the decision is made, and it is made once, on the server, so a signup form, an import script and a bulk run all branch the same way.

## Why a boolean, when the answer has 49 fields

Because a verification step needs a decision, and every code it sends costs money. SMS pumping works on that cost: a script submits numbers in ranges where someone earns a share of every message delivered, usually in countries the business has no customers in, and the bill arrives with the carrier invoice. The defence is not one field. It is the country the number belongs to, the kind of line it is, whether an SMS-receiving website publishes it, what your own delivery reports said about it, and how many numbers your account has tried in the same range lately. A team that reads those fields one by one ends up with an if-chain that drifts between the signup form, the password reset and the marketing sender. A policy puts the chain in one place and leaves `blocked_by` in the answer, so the reasoning stays visible.

Seven rules make up a policy, all optional.

| Rule                 | What it does                                                                                                     |
| -------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `allowed_countries`  | Send only to numbers of these regions. Null sends to any.                                                        |
| `blocked_countries`  | Never send to numbers of these regions.                                                                          |
| `blocked_line_types` | Refuse these line types, such as `premium_rate`, `pager` or `voip`.                                              |
| `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.                                                  |
| `require_reachable`  | Pass only a number a live carrier check reached.                                                                 |
| `block_opted_out`    | Refuse a number your suppression list holds as opted out. On by default.                                         |

They are applied in a fixed order: an invalid number is blocked first and always, then an opt-out, then the country rules, then the line types, then the first blocked signal in the published weights' order, then the score ceiling, and last the live-check requirement. `blocked_by` names the first rule that fired: `invalid`, `opted_out`, `country`, `line_type`, the signal's own name, `risk_score`, `unreachable` or `unconfirmed`. A batch, bulk or monitored run answers `suppressed` for a number served from your suppression list.

## The defaults

Until an account sets a policy, the published defaults apply. They block the five signals that almost always mean a code is wasted or abused, `fictional`, `disposable`, `reported_abuse`, `reported_abuse_widely` and `premium_rate`, and any score over 60, and they refuse a number your suppression list holds as opted out. They send to every country and every line type, and they require no live check. A number in a block held by a virtual-number wholesaler scores 40 and passes; so does a VoIP number at 30.

The test numbers show the defaults at work without spending a credit. `+1 202 555 0103` is the disposable scenario and answers `ok_to_send: false` with `blocked_by: "disposable"`; `+1 202 555 0105` is fictional and answers `blocked_by: "fictional"`; `+1 202 555 0102`, the virtual number, answers `ok_to_send: true` with a score of 40. The guest demo and the sandbox key answer under the defaults, because neither has an account to keep a policy. Every lookup a secret key makes, the test numbers included, answers under the account's stored policy, which makes them a way to try a policy before a customer meets it.

```json
{
    "e164": "+12025550103",
    "valid": true,
    "is_disposable": true,
    "risk_score": 70,
    "ok_to_send": false,
    "blocked_by": "disposable"
}
```

## Countries: the first control against pumping

SMS providers tell their customers to switch off every destination they do not serve, and a country allow-list is the same control one step earlier, before the code is requested. It is a business choice about where you send, not a verdict that a country's numbers are fraudulent: a bank in Ireland that only ever onboards Irish and British customers loses nothing by refusing a code to anywhere else, and a travel app would lose customers doing the same.

The country is the number's own, read from its calling code and prefix, which matters in two places people get wrong. The North American plan puts the United States, Canada and much of the Caribbean under +1, so `+1 876` is Jamaica and not the United States; an allow-list of `US` and `CA` refuses it. The United Kingdom's +44 also carries Jersey, Guernsey and the Isle of Man, which libphonenumber reports as `JE`, `GG` and `IM`, so a British service that serves the Crown dependencies names them too. A number with no country at all, such as the satellite and international-network codes, is outside every allow-list. Each region's page under [phone facts by country](/tools/phone-number-lookup/countries) names the regions that share its calling code, as the pages for [the United States](/tools/phone-number-lookup/countries/us) and [the United Kingdom](/tools/phone-number-lookup/countries/gb) show, so an allow-list can be checked against them before it goes live.

```bash
curl -X PUT https://spaw.co/api/v1/phone/policy \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"allowed_countries": ["GB", "IE", "JE", "GG", "IM"], "blocked_line_types": ["premium_rate", "shared_cost"]}'
```

Send only the keys you are changing. A `null` sends to any country again or clears the ceiling, an empty signal list blocks on no signal, and a body naming none of the seven rules answers `422`, because that is the shape a misspelled key takes. An empty allow-list is refused rather than read as "nowhere". The SMS policy tab on the phone verification page edits the same seven rules.

## Line types, and the one you cannot block

Blocking a line type is for numbers that cannot receive what you send: a premium-rate or shared-cost number costs the caller extra, a pager cannot show a code, and a toll-free number is rarely anybody's own handset. In the United Kingdom the 084 and 087 ranges answer `shared_cost`, because Ofcom's numbering plan classes them as service numbers with a capped charge and keeps premium rate for 09; that is why the example above lists `shared_cost` beside `premium_rate`, and why the default `premium_rate` signal alone lets them through. Blocking `voip` is a stronger choice than it looks, because a great many real people use VoIP numbers from their carrier or their employer; prefer the `virtual` signal, which names the block holder, and the score.

`fixed_line_or_mobile` can never be blocked. It is what the numbering plan answers when it cannot tell a mobile from a landline, which is most of North America, so a rule against it would refuse most American customers rather than any kind of line. Where the regulator's block table names a wireless carrier or a virtual-number wholesaler as the holder, the answer settles the type to `mobile` or `voip` and a line-type rule applies to it.

## Opt-outs, and when to switch the rule off

When a recipient replies STOP, or your SMS provider reports an opt-out code, the report puts the number on your suppression list with reason `opted_out`. A batch, bulk or monitored run then serves it from the list as `suppressed`. A single lookup still answers the number in full, with `meta.opted_out: true`, and under the defaults `block_opted_out` makes that answer `ok_to_send: false` with `blocked_by: "opted_out"`, so a marketing send never reaches someone who asked it to stop.

A sender of one-time codes or other transactional messages may want the rule off. An opt-out from marketing does not always forbid a verification code, and the person typing the number into your form may be its owner asking for a code again. Switch it off with `{"block_opted_out": false}` and the number is decided like any other while `meta.opted_out` still tells you it opted out; keep it on for the flows that send anything else. Either way the rule reads your own list, not a consent register, and honouring an opt-out stays your obligation.

## What the policy does not do

It does not change the verdict, the score or any field; it adds two. It is not consent: a number that passes is one you may send to as far as fraud and deliverability go, and whether the person agreed to hear from you is still yours to record. It is not a do-not-call scrub, and no field in the answer is one. It does not know your product either: a policy is a floor under your judgement, not a replacement for the segment-level decisions the fields support.

The live-check rule deserves care. It passes only a lookup made with `hlr: true` that reached the handset, answers `unreachable` when the network could not reach it, and answers `unconfirmed` when no live answer came back. While the live check is not enabled on the service, that is every lookup, so leave it off until you ask for the check on every lookup you gate.

## What to do next

- Read the SMS policy section of the [phone intelligence docs](/docs/phone-intelligence#policy) for the contract and the decision order.
- Read yours with [`GET /api/v1/phone/policy`](/docs/api/get-phone-policy) and change it with [`PUT`](/docs/api/update-phone-policy).
- See which signals catch OTP abuse in [stop OTP abuse with block data and disposable numbers](/guides/stop-otp-abuse-with-block-data-and-disposable-numbers).
- Put the check in front of a sign-in platform's code with the recipes for [Firebase Authentication](/integrations/firebase-auth), [Supabase Auth](/integrations/supabase-auth), [Amazon Cognito](/integrations/amazon-cognito) and [Auth0](/integrations/auth0).
- See what the defaults let through against SMS pumping, and the request that tightens them, in [SMS pumping: what the default SMS policy lets through](/guides/sms-pumping-what-the-default-sms-policy-lets-through).
- Read [why US numbers cannot be split into mobile and landline](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline) before you write a line-type rule for North America.

Reference: https://spaw.co/guides/gate-sms-verification-codes-with-the-sms-policy
