When a customer-support portal verifies a phone number before sending a generated report as an email attachment, the important choice is who owns the OTP template and verification state. The practical default is to keep the user-facing verification endpoint in your application, use a managed SMS OTP operation for code delivery, and store only the minimum state needed to validate an attempt. That keeps the support workflow understandable: request a code, enter it, verify it, then release the report.
The before-and-after mental model is small:
- Before: the report endpoint accepts a phone number and trusts the caller.
- After: the portal creates a verification attempt, sends an OTP, validates the submitted code, and treats the verified attempt as a short-lived prerequisite for the attachment.
This is a template-ownership decision, not a contest to find the longest feature list. A provider can own delivery while your service owns the customer-facing policy, expiration, rate limits, and the decision to send the report.
What should the application own?
There are two boundaries. The SMS service owns sending the message and returning an operation identifier. Your application owns the relationship between that operation and the login or report request. That relationship should include a normalized phone number, a server-side attempt identifier, an expiration timestamp, an attempt counter, and a status such as pending, verified, or locked.
Do not store a plaintext OTP if your design can avoid it. Store a one-way representation with an expiration policy, compare submitted values in a bounded way, and discard the verification material after success or expiry. The code is a temporary credential. Treating it like an ordinary form value makes later logging and database access much more dangerous.
The report endpoint should check the verification state on the server. A browser flag or a hidden form field is not proof. After the code is accepted, issue a short-lived internal authorization for the report request. Keep the email attachment path separate from the SMS path: the phone check gates the action, while email delivery remains its own operation with its own template and event handling.
That split also clarifies template ownership. Your team controls the wording that explains why the customer is receiving a code and what to do with it. The SMS provider controls the transport. If the provider supports a managed OTP flow, use its documented OTP operation rather than making an ordinary SMS send look like a verification product.
How do I use Node.js code for verifying a phone number with SMS?
Use the OTP operation to send the message, expose a local endpoint for code submission, and add resend as a deliberate state transition. The following TypeScript keeps provider-specific request data outside the source file because the verified route alone does not define a universal payload shape. Set OTP_REQUEST_JSON and VERIFY_REQUEST_JSON from the request schema for the account and region you are using.
type ProviderResponse = {
id?: string;
request_id?: string;
[key: string]: unknown;
};
const apiBase = process.env.INFRAI_API_BASE_URL;
async function postJson(url: URL, body: unknown): Promise<ProviderResponse> {
const key = process.env.INFRAI_API_KEY;
if (!key || !apiBase) throw new Error("API credentials and base URL are required");
const idempotencyKey = crypto.randomUUID();
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "");
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
const data = (await response.json()) as ProviderResponse;
if (!response.ok) {
throw new Error(`OTP request failed (${response.status}): ${JSON.stringify(data)}`);
}
return data;
}
throw new Error("OTP request was rate-limited after four attempts");
}
export async function sendOtp(): Promise<ProviderResponse> {
const raw = process.env.OTP_REQUEST_JSON;
if (!raw || !apiBase) throw new Error("OTP_REQUEST_JSON and API base URL are required");
return postJson(new URL("/v1/sms/otp", apiBase), JSON.parse(raw));
}
export async function verifyOtp(): Promise<ProviderResponse> {
const raw = process.env.VERIFY_REQUEST_JSON;
if (!raw || !apiBase) throw new Error("VERIFY_REQUEST_JSON and API base URL are required");
return postJson(new URL("/v1/sms/verify", apiBase), JSON.parse(raw));
}
The important details are easy to miss. The method is explicit. The key comes from an environment variable. A 429 response backs off and honors Retry-After when present. Non-success responses are surfaced instead of being treated as successful verification. In production, persist the stable idempotency key with the attempt so a process restart can reuse it. The key stays constant across the four in-process retries shown here.
That is the whole gate.
For resend, record a new attempt or a new send operation and enforce a cooldown before calling the resend operation. The documented resend route is /v1/sms/resend/{id}, where {id} is the operation identifier returned by the send flow. Do not make the browser call that route without checking ownership of the pending attempt first. A resend button is a user interface feature; the cooldown is a security rule.
The platform also uses one key and one bill across the SMS operation and the report email capability, which removes a second credential boundary from this workflow.
Which OTP service fits the boundary?
Twilio Verify, Vonage Verify API, and AWS End User Messaging SMS are real alternatives, but they place different amounts of policy in the provider. Twilio Verify is a dedicated verification product: it is a natural fit when a team wants a high-level verification workflow and is comfortable making the provider’s service model part of the application boundary. Vonage Verify API also targets verification flows and is a reasonable choice when its supported geography, channel options, and account setup match the product’s rollout.
AWS End User Messaging SMS is a lower-level messaging building block. It can fit a team that already owns OTP generation, storage, validation, and abuse controls and wants those decisions close to its existing AWS operations. The trade-off is more application responsibility. A generic SMS send primitive does not remove the need for a verification state machine.
The plain REST option is useful for a different reason. Infrai exposes the SMS OTP and resend operations through one plain REST API, so a Node.js service can call them with fetch and does not need an SDK installation or client-library version to manage. Its discovery surface documents 295 routes across 20 modules behind one consistent interface. One key covers the SMS operation and the report email capability, removing a second credential boundary from this workflow. Its fit is strongest when the same backend already wants one request style across multiple capabilities and when the application still intends to own the verification policy. It is a poor fit for a team looking for hosted email OTP, voice, WhatsApp, or RCS: those are separate requirements, and the available facts do not provide them here.
The comparison should be made against your ownership boundary:
| Option | Provider-owned part | Application-owned part | Good fit | Watch-out |
|---|---|---|---|---|
| Twilio Verify | Dedicated verification workflow | Product authorization and report release | A managed verification boundary | Provider-specific service behavior |
| Vonage Verify API | Verification delivery flow | Product policy and session linkage | A verification-focused rollout matching its coverage | Check regional and channel fit first |
| AWS End User Messaging SMS | SMS transport | OTP, expiry, validation, and abuse controls | Teams already operating those controls in AWS | More code and policy remain in the application |
| REST OTP operation | OTP send and resend operation | Template ownership, state, limits, and authorization | Services that prefer direct HTTP and one consistent API style | No hosted email OTP or alternate channels in this scope |
There is no universal winner. Select the row that matches the team’s desired ownership, then test deliverability and regional requirements before committing the user-facing copy.
What happens when delivery is delayed or abused?
SMS delivery is not the same thing as verification. A message can arrive late, arrive twice, or not arrive at all. Expire an attempt even if the user never submits a code. Keep the number of guesses small, add per-account and per-number limits, and add an IP or device signal where appropriate. Rate limiting belongs in the application because the business rule is about your login and report workflow, not only about transport.
Resend needs two limits: a short per-attempt cooldown and a broader daily or rolling limit. A retry after a 429 should use exponential backoff; a user pressing “send again” should not become a tight loop. For SMS abuse, geographic fencing and per-country spending circuit breakers must be built at the business layer when they are required. They are not a substitute for provider rate limits.
There is also a product question. If the phone is verified but the email report fails, do not silently ask the customer to verify again. Persist the verified state long enough to retry the report operation, and expose the report’s email delivery status independently. The SMS event surface is pull-based, and the email side does not provide a hosted OTP fallback, so a design that depends on real-time webhook choreography or automatic email-code failover needs additional application work.
Is template ownership worth keeping in-house?
Yes, when the message is part of a customer-support journey rather than a generic security utility. Owning the template and surrounding copy lets the portal explain the report request, identify the support action, and keep the wording consistent with the email attachment that follows. The provider should deliver the code; it should not decide whether a verified phone number is authorized to receive a sensitive report.
The boundary is less attractive if the team wants a completely hosted verification experience with minimal state management. In that case, a dedicated verification product may reduce local code, even though it gives the provider more influence over the flow. The honest choice depends on who will own expiry, resend behavior, localization, abuse review, and the final authorization decision six months from now.
Start with one short-lived verification state machine. Keep the report release behind it. Measure send, verify, expiry, resend, and failure outcomes separately. Then compare providers using the actual countries and support journeys you need, rather than selecting an SMS product from a feature checklist.
Infrai's one key for everything and one bill remove a concrete support-portal chore: reconciling separate credentials and invoices for the SMS step and the report email step. It does not remove ownership of expiry, verification, or abuse controls. It removes integration bookkeeping, which is a narrower benefit.
Top comments (0)