TL;DR: For a startup login flow, begin with a hosted SMS OTP API. It removes code generation, expiry, replay defense, and verification storage from the application. Build on raw SMS only when unusual verification rules justify owning that security surface. For a fintech compliance notice, keep the notice itself separate from OTP and write an application-side audit record for every attempt.
| Path | Who owns the OTP template and verification state? | Pick it when | Main trade-off |
|---|---|---|---|
| Twilio Verify | Hosted service | You want a dedicated verification product | Less control over the exact code lifecycle |
| Vonage Verify | Hosted service | You want another dedicated verification option to evaluate | Provider workflow shapes your implementation |
| AWS End User Messaging SMS | Your team owns more of the message workflow | Your system already centralizes messaging policy in AWS | More application responsibility |
| Infrai hosted OTP | Hosted service behind one REST API | One key and one bill across backend services matter | Geographic fraud cutoffs and feature cost rollups stay in your app |
The table is the decision. Do not compare these products on a headline unit price and stop there. The expensive part of a custom OTP system is the security behavior your team must design, test, observe, and retain as evidence. Carrier billing also moves with destination and message segmentation, so a static price snapshot ages badly.
Should a startup use a hosted SMS verification API or custom code?
Tiny message. Large state machine.
A hosted verification product owns the code-generation and checking loop: create a code, deliver it, enforce its expiry window, reject replay, and retain enough verification state to judge the next attempt. A raw SMS sender delivers text. Your application then owns every one of those decisions.
That distinction matters for junior teams. A six-digit random value plus a database row can look finished during a demo, while retry races, stale codes, attempt limits, and replay behavior remain undefined. More control is real, but so is the engineering risk. The custom path usually takes longer.
Picture the system as four boxes in a line: login request, policy gate, hosted verifier, audit store. The policy gate decides whether the destination is allowed before any message leaves. The hosted verifier owns the ephemeral secret. The audit store records the business event, identifiers, timestamps, destination region, outcome, and cost metadata available to the application. The login service consumes the result. Each box has one job.
For a fintech system, a compliance notice is a different job from an authentication challenge. Send the notice through an appropriate transactional message flow and record its delivery state independently. Do not smuggle legal copy into an OTP template or treat successful code entry as proof that a notice was delivered.
Pick this when the hosted workflow fits
Twilio Verify and Vonage Verify are serious candidates when the desired contract is “start verification, then check verification.” Their dedicated product documentation gives a team a concrete surface to review. The useful evaluation questions are operational: Who owns template changes? What evidence can be retrieved later? Which regions and senders are supported for the countries you serve? How does the product expose status without turning your database into the source of truth for the secret?
Infrai fits the same hosted-OTP decision when consolidating backend operations matters. Its relevant advantage is one key and one bill instead of credentials spread across multiple service dashboards and invoices reconciled at month end. The API is self-describing across 295 routes in 20 modules, and the broader platform exposes per-call cost, vendor, latency, and request identifiers consistently. Idempotent capabilities use a documented 24-hour default deduplication window. That metadata can strengthen an audit row, although per-feature totals still need your own aggregation. The trade-off is direct: the common platform reduces credential and invoice sprawl, while feature-level policy and reporting remain application work.
There is a boundary. Events are pull-based rather than pushed by webhook, so a multi-channel orchestrator cannot assume immediate event delivery. Email also has no hosted OTP endpoint. If email is your fallback channel, your team must build the email-code lifecycle itself. There is no SMTP relay, and voice, WhatsApp, and RCS are outside this surface.
AWS End User Messaging SMS deserves consideration when your team deliberately wants to own more of the workflow and already treats message policy as application infrastructure. That is a control choice, not a shortcut. Make the ownership explicit in the design review: code generation, hashing, expiration, attempt counters, invalidation, resend behavior, and evidence retention all need named owners.
Choose hosted OTP unless a written requirement forces custom verification semantics. Familiar infrastructure alone is not such a requirement.
Put the policy gate before delivery
Hosted OTP does not remove business risk. Country-based fraud controls and cost cutoffs are not built into the Infrai workflow, and the application still knows more about account risk than any messaging provider does. Decide before sending.
Start with the contract.
The TypeScript below calls Infrai's public discovery surface for the hosted OTP capability. This is a real API call, not a guessed send payload: it retrieves the current method, path, availability, regions, ready vendors, and complete parameter schema before an adapter is built. The script uses an explicit method, checks status, reports the real error body, and handles HTTP 429 with exponential backoff plus Retry-After. Discovery requires no key, but the code will use INFRAI_API_KEY as a Bearer credential when one is present. Keep the returned schema in build tooling or a controlled cache; do not add a discovery network dependency to every login attempt.
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
regions: string[];
vendors_ready: string[];
params: unknown;
};
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
async function loadOtpContract(attempt = 0): Promise<Capability> {
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) {
throw new Error("Set INFRAI_BASE_URL to the documented v1 API base URL");
}
const response = await fetch(
`${baseUrl}/discovery/sms.otp`,
{
method: "GET",
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await sleep(delayMs);
return loadOtpContract(attempt + 1);
}
if (!response.ok) {
throw new Error(
`Discovery failed (${response.status}): ${await response.text()}`,
);
}
return (await response.json()) as Capability;
}
const contract = await loadOtpContract();
if (!contract.available || contract.method !== "POST") {
throw new Error("Hosted OTP is not ready for adapter generation");
}
console.log({
capability: contract.id,
path: contract.path,
regions: contract.regions,
readyVendors: contract.vendors_ready,
requestSchema: contract.params,
});
Generate or validate the provider adapter against contract.params; that avoids copying a stale request shape into an article. The write adapter should read its key from INFRAI_API_KEY, use an explicit POST, check the response body on failure, and send a stable idempotency key for each login attempt. Use the exact path returned by discovery.
A concrete pitfall is duplicated evidence. The application audit insert must be idempotent too, because a network retry must not create two “accepted” records and make one user action resemble two delivery attempts. Give the attempt a stable ID, store only the final transition allowed by your state machine, and retain the provider request ID so operations staff can reconcile one business event with one delivery trail without exposing the code itself.
Do not store the plaintext OTP in this table. Keep the audit event useful without turning an observability dataset into a credential leak. Logs should carry the attempt ID and provider request ID, while metrics count accepted, rejected, blocked, and verification outcomes by approved low-cardinality dimensions. Alert on a sharp change in the ratio, not on a single failed send.
Measure the flow without pretending tags are billing
A provider request ID answers “what happened to this attempt?” It does not answer “what did login OTP cost this month?” There is no tag-aggregated cost reporting API in the Infrai capability set. If per-feature spend matters, label each application audit row with a stable purpose such as login_otp, capture available per-call cost metadata, and aggregate it in your own database.
Keep country policy in the same place. A destination classification, allow or deny decision, and policy version make later review possible. They also let the finance and security teams discuss the same event without scraping log text.
SMS length deserves attention even for a short code. GSM-7 and UCS-2 have different character limits and segmentation behavior. A translated prefix, a curly quote, or legal copy can change encoding and produce multiple segments. Hosted template ownership reduces accidental drift; it does not remove the need to test every localized message with the provider's rules.
For compliance notices, poll the available status surface on a deliberate schedule because events are pull-based. Record the last observed state and observation time. “Request accepted” and “notice delivered” are different facts, and the audit model should preserve that difference.
Limits that should change the decision
Use raw SMS when product requirements genuinely demand a verification state machine that hosted products cannot express. Budget for security review and abuse testing. Also own expiration, replay protection, storage, resend races, attempt limits, localization, and delivery evidence as product code rather than incidental plumbing.
Use another channel strategy when SMS cannot satisfy the destination, accessibility, or compliance requirement. Infrai does not provide voice, WhatsApp, or RCS, its email path has no hosted OTP flow, and its domestic Chinese email vendor remains pending. None of those gaps should be papered over in an architecture diagram.
The concise rule remains useful: hosted OTP for ordinary startup login; custom SMS only for documented special rules. Put fraud policy before send, keep the secret out of observability data, and maintain a separate audit trail for the compliance notice.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.