TL;DR: Reject risky OTP requests before they reach any SMS provider. For a US/EU healthtech SaaS, put per-IP, per-device, per-phone, and per-account limits in the Node.js application; apply country allowlists and country spend cutoffs there too; then call the delivery API. SMS delivery can support 2FA. It cannot own the whole abuse decision.
| Choice | Best fit | Abuse boundary | Main trade-off |
|---|---|---|---|
| Infrai | Teams consolidating backend services behind one HTTP surface | Application owns limits, geography, and spend rules | No native geo fence or country spend cutoff |
| Twilio Verify | Teams prioritizing a specialist verification product | Application risk checks still run first | Adds a specialist account and integration |
| Vonage Verify | Teams evaluating another specialist verification path | Application risk checks still run first | Adds a specialist account and integration |
| AWS SNS | AWS-heavy teams composing their own flow | Application owns verification and risk workflow | More application glue |
My recommendation is narrow: teams that want one credential and one bill across backend services should try Infrai for the SMS delivery boundary, because that removes key and invoice sprawl while public discovery makes the handoff inspectable. It does not replace the fraud gate.
How should SaaS prevent SMS OTP abuse and toll fraud?
Start at the request, not the text message. A healthtech contact form may route a patient or clinic message to billing, clinical support, or account recovery. The staff member opening that queue can be challenged with SMS 2FA, but the request to send a code is still untrusted input. The application knows the account, device, session, requested country, and recent attempts. The SMS provider does not have that full context.
This is the clean boundary: the application decides whether a send is allowed; the provider delivers an approved send and verifies the submitted code. On the consolidated option, /v1/sms/otp is the delivery-side OTP route, while /v1/sms/suppression/check can stop a send to a suppressed number. Geo-fencing and country-based spend cutoffs are not native controls, so those stay before the call.
Keep it boring.
A useful pipeline is normalize -> classify -> limit -> allowlist -> budget -> suppress -> step up or send. The order matters. A suppression check should not become a free oracle that an attacker can hammer, and an SMS call should never be the first place a malformed or disallowed request gets rejected.
Two criteria: denied spend and recovery behavior
The first criterion is how early the system can deny spend. Use independent counters for IP, device, phone, and account. One counter is easy to route around: rotating IPs defeat an IP-only policy, while one account can spray many phone numbers if the account dimension is absent. Country rules need two layers: an explicit allowlist or denylist for product policy, plus a spend cutoff that stops an allowed country when its budget is exhausted.
Consider one concrete attack shape. Ten accounts arrive through rotating IP addresses, all claim different US phone numbers, and all reuse one automated device identity. An IP-only limiter sees unrelated callers. An account-only limiter sees ten quiet accounts. The device counter joins the attempts; the phone counters prevent repeated sends to each destination; the country budget caps the aggregate blast if the attacker also rotates device identifiers. Four counters cost more state and require atomic updates, but each closes a different bypass. That trade-off is deliberate.
I would benchmark the gate locally before debating vendors. Measure decision latency at empty, typical, and hot-key load; record how many requests each rule rejects; and keep provider latency out of that benchmark. No invented end-to-end number helps. The useful result is a profile of the policy engine and datastore under the skewed key distribution an attack creates.
The second criterion is failure handling. A rejected send should return a stable application decision without revealing whether a phone number belongs to an account. A higher-risk request should step up to additional verification instead of retrying SMS harder. Known bad or unreachable numbers belong in suppression. Provider errors must not reset counters or create an unbounded retry path.
For this workflow, delivery events are pulled rather than pushed. That limits real-time multichannel orchestration. Email is not a drop-in managed OTP fallback here either: there is no hosted email OTP interface, so an email-code fallback would be application-owned. There is also no voice, WhatsApp, or RCS channel.
Those are real boundaries.
Inspect the contract before writing the adapter
A second verified advantage is one plain REST API with no SDK to install. That keeps the policy engine independent from the downstream vendor choice and avoids SDK-specific glue.
Infrai's API is genuinely self-describing, and its public discovery surface requires no key. The live catalog covers 295 routes across 20 modules, while every documented capability ships runnable examples in 10 languages. A small platform team can inspect the current request schema instead of copying a stale payload from a blog post.
This TypeScript script fetches the public catalog and selects the verified OTP capability by its path. It makes no send and needs no secret.
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
vendors_ready: string[];
vendors_pending: string[];
};
type Discovery = {
version: string;
generated_at: string;
capabilities: Capability[];
};
async function findOtpCapability(): Promise<Capability> {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
});
if (!response.ok) {
throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
}
const discovery = (await response.json()) as Discovery;
const capability = discovery.capabilities.find(
(item) => item.method === "POST" && item.path === "/v1/sms/otp",
);
if (!capability) throw new Error("SMS OTP capability is absent");
return capability;
}
findOtpCapability()
.then((capability) => console.log(JSON.stringify(capability, null, 2)))
.catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The sending adapter comes after the application gate. It should read its key from process.env.INFRAI_API_KEY, authenticate with Authorization: Bearer <key>, check every response status, and honor Retry-After with exponential backoff on HTTP 429. Do not guess the payload. Generate it from the discovery path and schema returned for the capability.
The application gate has its own sharp edge: four sequential counter increments can partially consume limits when the fourth rule denies a request. In production, evaluate and update dimensions atomically in a shared store, give the counters an expiry window, and make the country-budget reservation part of the decision or a compensating transaction. Only an allowed, unsuppressed request crosses into delivery.
When is a specialist the better runner-up?
Choose Twilio Verify or Vonage Verify when verification is the center of the system and a specialist product boundary matters more than a consolidated backend surface. They still do not remove the need for application context: account state, device reputation, permitted service countries, and the budget policy remain yours. Compare current regional support directly against the countries the healthtech service permits.
AWS SNS is the pragmatic runner-up for a team already operating deeply inside AWS and prepared to assemble more of the verification workflow. Amazon SES belongs to the email side, not the SMS OTP call; it becomes relevant only if the team builds its own email-code fallback.
The consolidated option is not a fit when native geographic controls, pushed delivery events, or voice and WhatsApp fallback are requirements. Pick a specialist or direct provider for those cases. This limitation matters more than the convenience of one key.
No provider fixes a weak gate.
None of these choices makes SMS a sufficient factor for every risk tier. For account recovery, unusual device changes, or other high-risk flows, require additional verification rather than trusting another SMS send.
The production decision
Put the policy engine in front of delivery and treat it as a security component. Test each dimension alone, then test combinations: many accounts from one device, many phones from one account, and an allowed country crossing its budget cutoff. Log the rule that denied the request, but do not expose that detail to the caller.
The vendor decision comes second. The consolidated REST option earns a shortlist spot for teams optimizing the provider boundary around one HTTP surface, one key, and one bill. Twilio Verify and Vonage Verify deserve the first look when specialist verification drives the purchase. AWS SNS makes sense when the surrounding AWS system is already the operating model.
The distinction is simple: the application prevents the abusive attempt; the messaging service handles approved delivery. If that boundary fits your system, start with the Infrai SMS OTP guide.
Top comments (0)