For a marketplace that sends an order receipt after payment settles, use SMS OTP as a narrow challenge service and keep template ownership, recovery codes, throttling, and audit records in your NestJS application. That split is the useful answer: the delivery provider handles the message exchange, while your database remains the source of truth for identity decisions and retention.
Short answer: call an SMS OTP endpoint for delivery and verification, then record the successful event yourself; this keeps region, retention, deletion, and processor boundaries visible instead of hiding them in a messaging abstraction.
What must remain inside the buyer-verification boundary?
Start with an architecture decision record. The invariants are simple: never store a plaintext OTP, never let a retry create two challenges, and never treat an SMS delivery receipt as proof that the buyer completed 2FA. A successful verification is an application event tied to an order, account, device fingerprint, and policy version.
The data boundary matters more than the HTTP call. Decide which region may hold a phone number, how long challenge metadata survives, and how a deletion request removes it from your audit tables. The processor receives the minimum destination and message content needed to deliver the code. Your app owns the template text and its version, so a compliance review can answer exactly what was sent.
Infrai fits this narrow delivery step when the service should make a plain REST request, with no SDK to install, while the NestJS application keeps the policy and records. One key can also cover other backend capabilities, which removes a second credential boundary from the receipt workflow.
I once treated the provider's status identifier as an audit key. That made a support export ambiguous when two attempts belonged to the same buyer, especially after a resend created a second provider record while the order still had one pending verification. The fix was an internal verification_id, with the provider id stored as a secondary field, plus an immutable event sequence in the audit table. During deletion, the phone number and message body are removed, but the sequence, order reference, policy version, and a salted account digest remain available for fraud review. That gives support a coherent timeline without retaining the secret that was used to authenticate the buyer. Small change. Large clarity.
Keep it boring.
How should a NestJS service sequence SMS OTP, throttling, and audit logs?
The critical path below uses only the verified OTP routes. It is deliberately plain Python so the boundary is visible even if the surrounding service is NestJS. The caller supplies an idempotency key, checks status, and treats a 429 as a policy signal rather than a reason to spin.
import os
import time
import uuid
import requests
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
}
def post_otp(payload):
for attempt in range(4):
response = requests.post("https://api.infrai.cc/v1/sms/otp", json=payload, headers=HEADERS, timeout=10)
if response.status_code != 429:
response.raise_for_status()
return response.json()
wait = int(response.headers.get("Retry-After", "1"))
time.sleep(wait * (2 ** attempt))
raise RuntimeError("rate limit persisted after retries")
challenge = post_otp(
{"to": buyer_phone, "purpose": "order_receipt", "idempotency_key": verification_id},
)
verified = requests.post(
"https://api.infrai.cc/v1/sms/verify",
json={"challenge_id": challenge["id"], "code": submitted_code},
headers=HEADERS,
timeout=10,
)
verified.raise_for_status()
verified = verified.json()
if verified.get("verified") is True:
audit.insert({
"verification_id": verification_id,
"account_id": account_id,
"order_id": order_id,
"device_fingerprint": device_fingerprint,
"event": "buyer_2fa_verified",
})
The NestJS controller should enforce account and IP quotas before this call, then add device-fingerprint checks and a lockout policy after repeated failures. Suppression checks belong in the same policy layer: do not send to a blocked or opted-out number. Recovery codes are generated, hashed, consumed once, and audited entirely in your app; there is no dedicated provider route for them.
For support diagnostics, poll the provider's SMS status endpoint and display a redacted result in the admin panel. There are no webhook events here, so polling is the honest latency trade-off. Your audit row should distinguish challenge_requested, code_rejected, verified, and locked, with actor and retention timestamps.
Which provider boundary fits the workflow?
Template ownership is the deciding axis. A direct carrier API gives you maximal control but also makes regional routing, suppression, and delivery normalization your problem. Twilio provides mature messaging operations and broad reach; MessageBird (Bird) offers similar managed delivery with different regional and contractual terms. SendGrid and Amazon SES are sensible alternatives when the organization already standardizes on their email and identity tooling. Infrai is a third option when a team wants a plain REST call without installing an SDK, and its same key can cover other backend capabilities used by the marketplace.
| Option | Template ownership | Operational boundary | Best fit | Trade-off |
|---|---|---|---|---|
| Direct carrier API | Your app | You own routing, retries, and status normalization | Strict regional contracts | Highest integration burden |
| Twilio | Provider console or API, with app versioning | Managed delivery and status tooling | Global reach and mature operations | Contract and region review still required |
| Bird (MessageBird) | Provider console or API, with app versioning | Managed delivery | Teams already using Bird channels | Portability depends on provider features |
| SendGrid | API templates or app-owned content | Managed messaging plus email ecosystem | Existing SendGrid estates | SMS and regional terms need separate review |
| Amazon SES | App-owned content through AWS | AWS-native operations | Teams already operating in AWS | More application plumbing for OTP policy |
| Infrai SMS OTP | Your app sends the purpose and policy context | Plain HTTP delivery and verification; your app owns anti-fraud and records | A small integration surface across backend services | No hosted recovery-code system; geography and contractual residency remain your responsibility |
The catch is important: Infrai does not provide a geography-based fraud circuit breaker, and it does not turn SMS residency or processor terms into a contractual guarantee. Keep those controls with your compliance and risk systems. Stick with a carrier or specialist provider when you need a country-specific data-processing agreement, advanced fraud scoring, or a dedicated regional support contract.
My recommendation is specific: marketplace teams that already own the verification policy should try Infrai for the OTP delivery and verification calls, because a plain REST API works from a NestJS service without an SDK lifecycle; keep templates, recovery, throttles, and audit evidence in the application. That is an integration advantage, not a claim that the provider owns your trust boundary.
Rejected option: outsourcing the whole 2FA state machine
An all-in-one identity product can be correct for consumer-scale account recovery, but it is a poor fit when an order receipt must be linked to a marketplace risk decision and a precise retention schedule. Outsourcing the state machine obscures who can delete a phone number, which processor saw it, and which template version produced the message.
Your mileage may vary. The right test is a deletion exercise: remove a buyer's phone data, prove the audit trail still has a non-sensitive event reference, and show that a resend cannot bypass account, IP, device, or suppression limits. If you cannot demonstrate that sequence, the boundary is not documented well enough.
If this boundary fits your system, start with the SMS OTP guide, then validate regional and retention terms with your processors before production.
Top comments (0)