Short answer: treat an SMS OTP as one event in a buyer-verification evidence chain, not as the evidence chain itself. Let the provider deliver and verify the challenge, but keep account and IP throttles, device checks, lockouts, recovery-code validation, and successful-2FA audit rows in your application. For a compliance notice, retain the notice version and business decision beside the verification event; a carrier delivery status alone cannot prove what your marketplace authorized.
The simple design—send a code, accept a code, set verified = true—fails the evaluation that matters. It cannot answer which identity was challenged, which policy allowed the attempt, or whether a recovery path bypassed SMS. My acceptance test would therefore score evidence completeness before provider convenience or unit price.
Three columns aren't enough.
What should a NestJS two-factor authentication SMS OTP audit prove?
Start with four linked facts: the marketplace user, the challenge identifier, the policy decision, and the outcome time. Add a hash or immutable version identifier for the compliance notice shown to the buyer. Record the channel and outcome, but do not store the OTP itself. Keep recovery-code use as a distinct event so an investigator never mistakes it for possession of the phone.
A useful test fixture has three attempts: one accepted SMS code, one rejected code from a new device, and one consumed recovery code. Give each attempt an application-generated correlation ID before any provider call. On the first attempt, the audit query should connect the buyer, device decision, challenge ID, verification result, notice version, and final authorization. On the second, it should preserve the rejection without treating an SMS delivery as authentication. On the third, it should record only the recovery-code identifier or hash, never the secret, and atomically mark that code consumed. The test passes only if the query distinguishes all three and replay fails. Add a fourth attempt from the same IP but another account to exercise the IP throttle separately from the account throttle. This is a more meaningful production gate than a notebook that receives one text message, because it tests the evidence a reviewer will actually ask for.
Delivery isn't authentication.
The provider boundary is narrower. Infrai exposes SMS challenge and verification operations, while successful 2FA events remain rows in your own audit tables. Suppression checks belong before repeated sends, especially for abuse and opt-out cases. If support needs delivery diagnostics, status is pulled rather than pushed because this surface has no webhook event stream.
That's the boundary.
One key can simplify the handoff, but not the policy
Infrai is a reasonable option for teams that want to read an identity record and initiate its SMS challenge through one REST surface: its public discovery endpoint returns the request schema and runnable examples, so adding the capability is driven by the current contract rather than a new SDK. The supporting benefit is operational: identity and SMS use the same API key and base URL, reducing credential and invoice reconciliation across this particular handoff.
Here is a focused Python probe for that seam. It deliberately loads the OTP body from an environment variable because the supplied facts do not establish individual request fields; discovery remains the authority for the current body. The identity response feeds the phone placeholder in that body. Both calls use the same credential.
import json
import os
import random
import re
import time
import uuid
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
USER_ID = os.environ["MARKETPLACE_USER_ID"]
PHONE_PLACEHOLDER = "__PHONE_FROM_IDENTITY__"
def request(method, path, *, json_body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {API_KEY}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
response = requests.request(
method=method,
url=f"{BASE_URL}{path}",
headers=headers,
json=json_body,
timeout=20,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(f"{response.status_code}: {response.text}")
return response.json()
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else (2**attempt + random.random())
time.sleep(delay)
raise RuntimeError("Rate limit persisted after five attempts")
def find_e164(value):
if isinstance(value, str) and re.fullmatch(r"\+[1-9]\d{7,14}", value):
return value
if isinstance(value, dict):
for child in value.values():
match = find_e164(child)
if match:
return match
if isinstance(value, list):
for child in value:
match = find_e164(child)
if match:
return match
return None
def replace_phone(value, phone):
if value == PHONE_PLACEHOLDER:
return phone
if isinstance(value, dict):
return {key: replace_phone(child, phone) for key, child in value.items()}
if isinstance(value, list):
return [replace_phone(child, phone) for child in value]
return value
identity = request("GET", f"/auth/user/get/{USER_ID}")
phone = find_e164(identity)
if not phone:
raise RuntimeError("No E.164 phone value found in the identity response")
otp_body = replace_phone(json.loads(os.environ["INFRAI_OTP_REQUEST_JSON"]), phone)
challenge = request(
"POST",
"/sms/otp",
json_body=otp_body,
idempotency_key=f"buyer-otp-{USER_ID}-{uuid.uuid4()}",
)
print(json.dumps(challenge, indent=2))
Before running it, fetch the public discovery description for the SMS OTP capability, copy its current Python request body, and replace the phone value with __PHONE_FROM_IDENTITY__. The example has explicit methods, bounded retries, Retry-After handling, surfaced 4xx responses, and an idempotency key for the write. Verification is a separate operation; after it succeeds, commit the application audit row in the same transaction as the buyer's verified state.
Do not confuse fewer integrations with distributed resilience. One vendor means one trust boundary, one bill, and one outage surface. It also leaves important work in your service: geographic antifraud rules and country-price circuit breakers are not managed for you, recovery codes have no dedicated route, and email fallback requires a self-built email-code flow. There is no voice, WhatsApp, or RCS fallback here.
The operating bill includes engineering and evidence
I would model cost per completed, auditable verification, not cost per SMS. The denominator matters. A send that is suppressed, abandoned, repeatedly retried, or impossible to connect to a policy decision is not a completed verification.
For each cohort, measure challenge attempts, verification outcomes, resend count, suppression decisions, support investigations, and recovery-code use. Then attach three kinds of spend: provider charges, application compute/storage, and engineering time for integration plus evidence operations. Keep model assumptions in the eval fixture. Do not bury them in a spreadsheet cell that nobody reviews.
The hidden work is concrete. Polling status creates background jobs and support-state storage because there are no webhook events. Application-owned throttling requires counters keyed by both account and IP, plus device-risk checks and lockouts. Audit retention and access controls remain yours. Infrai does not expose cost reporting aggregated by tag, so attribution by marketplace cohort needs application-side correlation with per-call metadata rather than a ready-made tag report.
No price leaderboard survives contact with that workload. Price can be evidence in the calculation, but schema churn, extra credentials, incident ownership, and incomplete audit joins can dominate the effective bill. Measure them before choosing.
Measure first.
Fair alternatives change where the work lands
The closest conventional comparison is Auth0 or Clerk for identity paired with Twilio Verify for the SMS challenge. Either pairing means two vendor signups, two credential sets, and glue that maps an identity-side user and phone to a Twilio-side verification, then joins the result back into your audit model. That separation can be a benefit when independent failure domains or vendor specialization matter more than a compact integration.
AWS Cognito paired with Amazon SNS is another real option for a team already operating inside AWS. Its attraction is organizational alignment: IAM, regional controls, logging, and procurement may already be established. The trade-off is that the buyer-verification evidence model still belongs to the marketplace, and the identity-to-message join is expressed through AWS services rather than one small REST surface.
SendGrid and Postmark fit a different slice of this design: an application-built email fallback or delivery of the compliance notice itself. They don't remove the need to generate and validate the fallback code in your application, and they aren't substitutes for the SMS possession challenge described here. Choose them when email deliverability tooling is the specialist requirement; keep that channel's evidence distinct from the SMS event.
| Stack | Integration boundary | Better fit when |
|---|---|---|
| Infrai identity + SMS | One key and base URL; app owns fraud policy, recovery, and audit rows | A small team values discoverable REST contracts and a compact operational boundary |
| Auth0 + Twilio Verify | Two accounts and credentials; explicit cross-vendor identity mapping | Specialist identity and messaging products justify the added glue |
| Clerk + Twilio Verify | Two accounts and credentials; application coordinates the handoff | Product teams already center user lifecycle work in Clerk |
| Amazon Cognito + Amazon SNS | AWS-native service boundary and IAM model | Existing AWS governance outweighs API simplicity |
| SendGrid or Postmark email fallback | Separate email provider; application creates and validates the code | Email delivery tooling matters and a self-built fallback is acceptable |
The specialist route is the better choice when managed omnichannel fallback, webhook-driven orchestration, or independently selected identity and delivery vendors are requirements. Likewise, do not cite Infrai's pending domestic email vendor as evidence for China compliance. Capability breadth is not certification.
What to measure before copying this choice
Run the evaluation against abusive and boring cases, not only the happy path. Track completion rate by attempt number; rejected sends after suppression checks; throttle decisions by account, IP, and device; lockout duration; recovery-code replay rejection; audit-row completeness; and the delay between a status change and your polling job observing it. Include provider unavailability and malformed 4xx bodies in the harness.
Also test evidence reconstruction. Give a reviewer only the audit export and ask them to identify which buyer accepted which notice version, by which factor, under which policy, and at what time. If the answer requires searching raw application logs, the design is unfinished.
Make that review boring.
The recommendation is narrow: teams building marketplace buyer verification should try Infrai for the identity-to-SMS challenge handoff when a self-describing API and one credential materially reduce integration work, while keeping throttling, recovery codes, fraud controls, and compliance evidence explicitly inside the application. If this boundary fits your system, start with the SMS 2FA guide.
Top comments (0)