message.comDevelopers

Prevent SMS fraud with Verify

SMS pumping is the OTP industry's open secret: an attacker scripts signups against your app, points them at numbers they control or lease in bulk, and collects a cut of the carrier fee every message you send generates. Most providers treat the defenses as a paid add-on or a support ticket. Verify ships six guards on by default, at no extra cost, and this page documents every one of them with its real number.

Nothing below is a plan upsell. Every guard on this page is active on every workspace from the moment you create a Verify Service, using the defaults shown. You can raise most of them; you cannot turn them off.

Why SMS pumping happens

An OTP send endpoint is attractive to abuse because it does something a normal API endpoint does not: it spends real money (a carrier fee) on every call, triggered by an unauthenticated action (a phone number typed into a form). If that endpoint has no limits, a script can enter thousands of numbers a minute, drain your Verify balance, and, when the destination numbers are complicit, split the carrier revenue with whoever is running the attack. The same shape applies to email flood and voice-call flood, at lower per-message cost. None of it requires guessing a code; the attacker never intends to receive one.

The fix is not a single filter. It is layered limits at every stage a request passes through, so an attacker who evades one guard still hits the next one. Verify's pipeline runs the guards below in a fixed order and stops at the first one a request fails, which also means a request that fails an early, cheap check (like a country block) never reaches a later, more expensive one (like the credit balance lookup).

Guard 1: country allowlist

Every Verify Service has an allowedCountries list. The platform default is ["US", "CA"]. SMS and voice destinations outside the allowlist are rejected with 403 country_not_allowed before a code is even generated, let alone sent. Email is exempt from this check entirely, since there is no per-message carrier cost to protect against.

Country resolution reads the destination's E.164 prefix. The +1 country code covers both the United States and Canada, so a +1 number is allowed if either US or CA is on the list. Numbers whose prefix does not resolve to a known country are rejected by default; a workspace can opt into a wildcard by adding "*" to its allowlist, which allows every country and should only be used once you understand the exposure it opens.

403 country_not_allowed
{
  "error": "country_not_allowed"
}

This is the cheapest guard to evade with a real number in an allowed country, which is exactly why it is first: it filters the largest, laziest slice of pumping traffic (numbers in countries you never intended to support) before spending any compute or database work on the request.

Guard 2: per-service velocity caps

Every Verify Service carries two send caps: perMinuteCap (default 10 sends per minute) and perDayCap (default 1,000 sends per day). Both are counted per service, across every destination that service sends to, and both are things you can raise if your legitimate volume outgrows them. Hitting either returns 429 rate_limited with scope: "workspace".

429 rate_limited, scope workspace
{
  "error": "rate_limited",
  "scope": "workspace"
}

This is the guard that catches a script hammering your signup form: even if every number it tries passes the country check, the aggregate send rate from your service trips the cap long before the attacker gets far.

Guard 3: workspace-wide daily ceiling

Above the per-service caps sits one more number: a workspace-wide daily ceiling, default 5,000 sends per day, summed across every Verify Service in the workspace combined. This exists so that creating additional services cannot be used to multiply the per-service caps: five services each capped at 1,000 sends a day would otherwise total 5,000 sends without the ceiling meaning anything. It fires as 429 rate_limited with scope: "workspace_ceiling".

429 rate_limited, scope workspace_ceiling
{
  "error": "rate_limited",
  "scope": "workspace_ceiling"
}

Unlike the per-service caps, this is not a field you can PATCH on a service. It is a workspace-level backstop; raising it is a support conversation, by design, so an attacker who compromises API-key-level access to one service cannot quietly raise their own ceiling.

Guard 4: destination and IP throttles

Between the workspace ceiling and this guard, the pipeline also checks that the workspace has a positive credit balance (402 insufficient_credits if not); that check is a billing gate rather than an abuse guard, so it is covered in Get started with Verify rather than here. The last abuse guard before a message actually dispatches is the narrowest one: no more than 3 sends to the exact same destination within 60 seconds, and no more than 10 sends from the same source IP within 60 seconds. Both fire as 429 rate_limited with scope: "destination", and either one can trip even after every earlier guard (country, velocity, workspace ceiling, credit balance) has already passed, because it runs at the point of send, not at the point of request validation.

429 rate_limited, scope destination
{
  "error": "rate_limited",
  "scope": "destination"
}

This is the guard that stops the specific pumping pattern of hammering one number (or a small pool of numbers behind one IP) to run up carrier fees quickly. It is also the guard your own UI is most likely to trip by accident: a resend button with no debounce, or a form that fires the start call on every keystroke, hits this limit against real users. Debounce resend actions and give users clear feedback that a code is already in flight; see retry UX in the best practices guide.

Guard 5: attempt limits on checking a code

Rate limiting the send side stops flooding; it does nothing about an attacker who receives a real code and tries to guess someone else's. Every verification allows five check attempts. The fifth wrong guess locks the verification permanently with too_many_attempts, regardless of whether the sixth guess would have been correct. A locked verification cannot be un-locked; the caller has to start a fresh one, which restarts the destination throttle above.

A wrong guess is not an HTTP error. Checking a code always returns 200; you read the verified field and, on failure, the reason. Full detail on every check-side reason, including the exact difference between bad_code, expired, already_used, and too_many_attempts, is on the Verify API error codes page.

Guard 6: billing on success only is an economic brake

This one is not a rate limit; it is a pricing decision that happens to double as a fraud control. Creating a verification never bills, no matter what happens to it: sent, failed to dispatch, expired, or never checked at all. The only event that bills is a successful POST /v1/verify/check, exactly once per code, at $0.03.

Billing on success only removes the economic motive for the most common form of pumping against a per-message-billed API: an attacker who never intends to receive or enter a code gets nothing from triggering sends against your workspace, because sends alone cost you nothing to fraudsters targeting your spend. The five other guards above exist to protect your carrier relationship and your users' inboxes, not your bill, which is a distinction most competitors' pricing pages do not make explicit.

This does not mean unlimited sends are free to you in every sense: a flood of unanswered sends still costs carrier goodwill and can affect deliverability for your legitimate traffic, which is exactly what guards 1 through 4 exist to prevent regardless of billing.

What is on by default, in one place

GuardDefaultConfigurable
Country allowlist["US", "CA"]Per service, via allowedCountries. Email is always exempt.
Per-service velocity10 sends/minute, 1,000 sends/dayPer service, via perMinuteCap / perDayCap.
Workspace daily ceiling5,000 sends/day across all servicesNot self-service. Contact support to raise it.
Destination throttle3 sends per destination per 60 secondsFixed. Not a per-service or per-workspace setting.
IP throttle10 sends per source IP per 60 secondsFixed. Not a per-service or per-workspace setting.
Attempt cap5 wrong guesses per verificationFixed. A locked verification cannot be reopened; issue a new one.
Billing triggerOnly a successful check bills, once, at $0.03Not configurable. Creating and failing a verification is always free.

What competitors bury and we do not

Most OTP providers document fraud protection as a marketing bullet ("advanced fraud detection") without a single number attached, or gate the actual controls behind an enterprise tier. Verify has no separate fraud-protection tier: every guard on this page runs on every workspace, on the free path, using the exact defaults above. The only things you can change are the ones that are supposed to be a business decision (which countries you serve, how much volume you expect), not the ones that are a security decision (the destination throttle, the IP throttle, and the attempt cap, which stay fixed for every workspace).

What is next, honestly

The guards on this page are what ships today. A prefix-level velocity circuit breaker and adaptive unverified-send backoff are designed as a follow-on hardening layer on top of the six guards above, not a replacement for them; nothing on this page changes when that layer lands; it adds detection for abuse patterns that spread across many destinations rather than concentrate on one.

Next steps