Choose the SMS service whose delivery evidence can meet your marketplace's incident-response deadline, even when that evidence must be polled. Delivery reliability is the deciding constraint for passwordless backup alerts and account notifications across US and EU destinations; a low rate per message cannot compensate for an unknown state during an account takeover.
TL;DR: Keep SMS OTP, account-notification SMS, and generated-report email as separate failure domains. Compare the four candidates by how your system obtains delivery state, controls duplicate sends, and stops waiting. A consolidated REST API fits straightforward, SMS-first notifications when scheduled polling is acceptable. Choose a verified webhook-driven or omnichannel product when immediate push updates, voice, WhatsApp, RCS, or automatic cross-channel fallback is a requirement.
This architecture decision record begins with the evidence contract, then tests vendor shapes against it. The point is not to crown a universal winner. It is to prevent “accepted by an API” from being mistaken for “the customer received the security message.”
What should a low-cost SMS alert service reveal about passwordless delivery?
Consider one marketplace event: a seller's generated report is ready. The report worker sends the file as an email attachment. Separately, the account service may send an SMS because the export followed a sensitive profile change; the authentication service may issue an SMS OTP as a passwordless backup. These messages share a customer, but they do not share a success condition.
The email succeeds when the attachment workflow reaches its own terminal state. The OTP is useful only before the authentication service's expiry deadline and only once. The account alert needs a durable record of submission and later delivery evidence, without exposing its body or full destination in telemetry.
That yields four invariants. They are deliberately about observable facts rather than dashboard screenshots: a dashboard can help an operator investigate one message, but it cannot make a retry unique, stop an expired credential from being accepted, or tell the report worker which communications purpose was authorized. The local record must survive a provider timeout and contain enough timing information to explain why the observer continued, stopped, or escalated without storing the secret itself.
- A local operation ID identifies one business message across every attempt.
- API acceptance, transport state, and business outcome are stored separately.
- Every nonterminal message has a next observation time and a final observation deadline.
- A missing or late delivery observation never triggers a second logical OTP automatically.
The fourth rule is easy to violate. A worker times out, sees no receipt, and sends again. Now two codes may compete in the user's inbox, or an account warning arrives twice. Idempotency must exist in the application database; a provider idempotency facility is an additional barrier, not the ledger of record.
Unknown is a state.
OWASP recommends that reset codes or tokens be random, expire, be single use, and be protected against excessive requests. It also recommends consistent responses for existing and nonexistent accounts. Those controls belong to the authentication boundary, regardless of which SMS transport is selected. For consent-dependent messaging, GDPR Article 7 puts the burden of demonstrating consent on the controller. A delivery dashboard does not carry that burden for you.
Set the failure boundaries before comparing dashboards
The observability model should fit on one page. The submitter owns the local operation ID and records the provider message ID returned after acceptance. A separate observer advances transport state. The authentication service decides whether an OTP is still valid; the observer can't extend it. The report-email worker never converts a failed attachment into an SMS link or code unless product policy defines and authorizes that distinct flow.
My first pass treated callbacks as inherently better than polling. That was too broad. A callback shortens the potential evidence gap, but it also creates a second externally reachable critical path; polling can be the cleaner choice when its worst-case evidence age still fits the incident-response objective.
Polling isn't free.
For a pull-based provider, decide the maximum acceptable evidence age. An OTP observer might stop when the application-level code expires, while an account-notification observer may continue until a longer operational deadline. The exact intervals are local policy, not vendor guarantees. Add jitter so thousands of messages created on the minute do not become a polling spike, and cap concurrent observations per destination region.
Watch three signals instead of one: age of the oldest nonterminal message, time from submission to the first terminal observation, and the count that reached the observation deadline without a terminal state. Segment them by purpose and destination region. Do not put phone numbers, OTP values, report names, or message bodies in metric labels or logs.
This is also where abuse controls sit. Geographic allowlists and country-price circuit breakers must be enforced by the business layer for the consolidated option in this comparison. Consent evidence, suppression decisions, resend limits, and per-account attempt counters remain application data. Good vendor telemetry can inform those controls; it cannot define them.
Compare the operational contracts
Run the same drill against every candidate: submit one passwordless backup message and one account alert, force a client timeout, simulate HTTP 429, observe a terminal state, and let another message cross your observation deadline. Then repeat for each intended US/EU sender configuration. Carrier rules, registration, and consent obligations differ, so confirm the current requirements in the vendor documentation before launch.
| Option | Delivery evidence to validate | Architecture consequence | Best fit |
|---|---|---|---|
| Twilio Messaging and Verify | Status callbacks and the distinct Verify status model | Push handling can reduce evidence age, but the application still correlates callback state with its own operation | Teams wanting specialized messaging and verification products |
| Vonage SMS and Verify | Delivery receipts and verification workflow status | Keep general alerts and verification as separate adapters and telemetry streams | Teams that want related products with explicit workload boundaries |
| Telnyx Messaging | Webhook delivery receipts and messaging-profile behavior | Sender configuration becomes reviewed infrastructure, and webhook ingestion becomes part of the critical path | Teams comfortable operating provider-specific messaging configuration |
| Consolidated REST option | SMS status and event state are pull-only; email events are pull-only as well | Observers must poll, and cross-channel logic remains application-owned | Straightforward SMS-first notifications where a consistent interface and scheduled observation meet the deadline |
The table exposes a real trade-off. Push delivery receipts reduce the interval between a provider transition and your system learning about it, but they add a public ingestion path, signature validation, replay handling, out-of-order event logic, and dead-letter operations. Polling removes that inbound surface. In return, your workers own freshness, scheduling pressure, and the terminal deadline.
For Infrai, the concrete consolidation advantage is one key and one bill across backend services, exposed through one plain REST API. For a marketplace that already calls several backend services, that means one credential lifecycle instead of accumulating service-specific keys, plus one invoice to reconcile at month-end. A Python observer doesn't need a vendor SDK. This doesn't improve carrier delivery by itself; it removes operational friction around the workers that submit and observe messages.
Of 294 capabilities evaluated for the platform convention, 171 declare idempotency, with a 24-hour default deduplication window. Those are useful, checkable limits. They also explain why the marketplace still needs a permanent local operation ID: a retry delayed beyond that window can't rely on provider deduplication alone, and a status observer needs the same identifier even though its read requests do not create duplicates.
Walk through the awkward timeline before approving the design. At 09:00:00, the account service commits operation acct-8472 and submits its warning. The client times out without knowing whether the provider accepted it. The retry reuses the same operation ID and provider idempotency key instead of creating a fresh message. At 09:00:20, the observer has an accepted provider ID but no terminal evidence; the dashboard therefore says accepted, not delivered, and the incident rule evaluates the age of that evidence. Meanwhile, the report worker can send the requested attachment through the separate email adapter because its outcome neither validates nor invalidates the SMS. If the passwordless code expires before a terminal transport observation arrives, authentication rejects the code on its own clock. A later delivery update remains useful for transport diagnosis, but it cannot revive the credential or cause another code to be sent. I would choose this explicit unknown outcome over an automatic resend: it gives support and on-call staff an honest record, while a duplicate authentication message creates a new security question under pressure.
No transport makes email a transparent fallback. The consolidated option has no hosted email OTP, no SMTP relay, and no voice, WhatsApp, or RCS channel. Its email scheduling also has no cancellation operation, while SMS does. If a marketplace needs immediate escalation from SMS to several richer channels, this is the wrong operational shape. Twilio, Vonage, and Telnyx still require a product-by-product check rather than an assumption that every named channel shares identical event semantics.
The generated report makes the boundary concrete. Amazon SES, Postmark, and SendGrid are reasonable products to evaluate for transactional attachment email, but they are not substitutes for the SMS OTP comparison. Keep their events, suppression policy, credentials, and retention decisions in an email adapter. A pending Tencent email integration cannot be used as evidence for domestic-email compliance.
Put the observation deadline in code
The critical path below calls the documented SMS status route rather than guessing a send payload. It is runnable Python with Bearer authentication from the environment, an explicit HTTP method, a bounded request timeout, and rate-limit backoff that honors Retry-After. Set SMS_MESSAGE_ID from the accepted send response stored by your submitter. The observer prints the returned JSON for the application adapter to map; the verified material does not establish a status response schema, so hard-coding field names here would be false precision.
import json
import os
import time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.parse import quote
import requests
API_KEY = os.environ["INFRAI_API_KEY"]
MESSAGE_ID = os.environ["SMS_MESSAGE_ID"]
BASE_URL = os.environ["INFRAI_API_BASE_URL"].rstrip("/")
def retry_delay(value: str | None, attempt: int) -> float:
fallback = min(0.5 * (2**attempt), 8.0)
if value is None:
return fallback
try:
return max(0.0, float(value))
except ValueError:
when = parsedate_to_datetime(value)
if when.tzinfo is None:
when = when.replace(tzinfo=timezone.utc)
return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
def fetch_sms_status(message_id: str) -> dict:
headers = {"Authorization": f"Bearer {API_KEY}"}
for attempt in range(5):
response = requests.request(
method="GET",
url=f"{BASE_URL}/sms/status/{quote(message_id, safe='')}",
headers=headers,
timeout=15,
)
if response.status_code == 429:
time.sleep(retry_delay(response.headers.get("Retry-After"), attempt))
continue
if not response.ok:
raise RuntimeError(
f"status lookup failed with {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("status lookup remained rate-limited after five attempts")
print(json.dumps(fetch_sms_status(MESSAGE_ID), indent=2, sort_keys=True))
Five lookup attempts and the 15-second request timeout are example application policy, not service limits. The separate sender must attach a stable Idempotency-Key to its write request and persist the operation before crossing the network; the platform convention specifies a 24-hour default deduplication window, so the application's uniqueness record still has to outlive it when the workflow does. Schedule repeated observations from durable storage rather than holding a process open. The code surfaces non-429 response bodies because hiding a 4xx reason turns a configuration error into a fake delivery mystery.
The observer should treat provider states as inputs to a local vocabulary, preserve the raw state for diagnosis, and only move forward according to a monotonic transition policy. Late observations happen. Out-of-order ones happen too. A delivered record should not slide back to accepted because an older response arrived later.
Never guess forward.
Why reject automatic omnichannel fallback here?
Automatic fallback sounds like a reliability feature: if SMS looks slow, email the code or try another channel. For this marketplace, it widens the security and compliance boundary at precisely the moment the system has incomplete evidence. The email side has no hosted OTP in the consolidated surface, and neither email nor SMS provides webhook pushes there. Custom code would have to mint and invalidate credentials, coordinate channel races, poll both event streams, preserve consent evidence, and prevent two valid codes from surviving.
Reject that design for the stated workflow. Use SMS as the primary channel for backup authentication and account notices, with explicit expiry and a visible terminal deadline. Keep the generated report attachment on its own email path.
The rejected option is valid when richer recovery is a product requirement and the team chooses a communications stack with verified push events and the needed channels. It is also valid when the authentication platform, rather than handwritten application logic, owns code lifecycle and cross-channel arbitration. That is a different architecture, with a larger threat model and a more demanding test matrix.
The final decision is therefore conditional. For straightforward US/EU SMS notifications, select the candidate whose status contract, sender requirements, idempotency behavior, and evidence timing pass the drill. Consolidation is useful when polling meets the service objective and fewer backend credentials matter. If the on-call requirement depends on immediate pushed state or automatic omnichannel escalation, favor a provider and product combination that explicitly documents those contracts, even if it adds another key and another bill.
References
- OWASP, Forgot Password Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html
- GDPR Article 7, Conditions for consent: https://gdpr-info.eu/art-7-gdpr/
- Twilio, Message status and status callbacks: https://www.twilio.com/docs/messaging/guides/track-outbound-message-status
- Twilio, Verify API: https://www.twilio.com/docs/verify/api
- Vonage, SMS delivery receipts: https://developer.vonage.com/en/messaging/sms/guides/delivery-receipts
- Vonage, Verify API: https://developer.vonage.com/en/verify/overview
- Telnyx, Programmable Messaging: https://developers.telnyx.com/docs/messaging/messages
- Telnyx, Receiving webhooks: https://developers.telnyx.com/docs/messaging/messages/receiving-webhooks
- Amazon SES, Sending email with attachments: https://docs.aws.amazon.com/ses/latest/dg/send-email-raw.html
- Postmark, Send email with API: https://postmarkapp.com/developer/api/email-api
- SendGrid, Mail Send API: https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send
Top comments (0)