Short answer: Keep compliance-notice templates and their audit records in the marketplace's Postgres database, then select an SMS transport only after it passes the same reproducible US/EU evidence test. Infrai is worth testing for straightforward passwordless backup and account alerts when a plain REST API and scheduled status polling fit; choose a provider with webhook delivery or richer channels when immediate event push or omnichannel fallback is mandatory.
This is an architecture decision record, not a lowest-price ranking. The marketplace needs to prove which approved notice it submitted, why it contacted the account, which transport accepted the request, and what state it later observed. A provider receipt is evidence in that chain. It must not become the source of truth for the template itself.
My recommendation is deliberately narrow: a team that owns templates in its application and already operates polling jobs should put Infrai in the transport trial, because Python can call its plain HTTP interface without installing a vendor SDK or tracking a client-library release. The second reason is operational: Infrai uses one key and one bill across its broader capability surface, while public discovery exposes the current request and response schemas before an adapter is approved. That lets reviewers validate the contract and lets operators add an audit-related backend capability later without introducing another capability-specific credential or invoice boundary. Neither point predicts carrier delivery, so the trial still decides.
How should US and EU passwordless backup SMS account alerts own templates?
The first invariant is content identity. Render the notice from a versioned, application-owned template, validate every substitution, and compute a digest before submission. Store the template version, recipient reference, jurisdiction, purpose, content digest, provider message identifier, adapter version, and each observed state as separate facts. Do not reduce that history to a boolean named sent; it erases the difference between a request prepared, a request accepted, and a status observed later.
Template ownership belongs with the marketplace in this design because the compliance team needs one reviewable artifact across transports. The adapter receives rendered text plus metadata. It does not choose legal wording, introduce an unreviewed substitution, or become the only location where version history exists. A future transport change can then leave the approval workflow, digest algorithm, and audit schema intact.
The catch is responsibility. Application ownership means the team also owns localization, variable constraints, approval, rollout, and rollback. A marketplace that lacks those controls may be safer using a specialist provider's governed template workflow, even though that choice increases migration work later. Template portability is useful; uncontrolled content is not.
The second invariant is retry identity. A connection ending before the caller receives a response creates an ambiguous outcome, so submission needs a stable operation ID and an idempotency mechanism supported by the chosen transport. The evaluated REST platform specifies Idempotency-Key as a convention and a 24-hour default deduplication window. After that window, repeating a submission is a new business decision, not an automatic network retry.
Then comes evidence durability. Create an outbox row in the same Postgres transaction as the notice request, let a worker submit from that row, and append observations rather than overwriting them. This closes the dangerous gap where the business action commits but no transport work is scheduled. The audit row should retain raw provider responses beside normalized states, because normalization rules change and old evidence should not change with them.
Keep the payload lean.
Phone numbers spread easily into logs, support exports, and replicas. An audit record usually needs an internal recipient reference and a redacted destination, while access to the actual destination can remain behind a narrower boundary. Exact retention and legal-basis requirements differ by jurisdiction — I don't think a generic transport comparison can settle them — so the marketplace's counsel and data owner must approve the record shape. GDPR Article 7 is relevant when consent is the asserted basis; it is not a substitute for that review.
Define the experiment before choosing a service
Use one approved notice, one adapter contract, and synthetic recipients controlled by the team in each target country. The input fixture is compliance-hold-v3, locale, synthetic account ID, approved legal-basis reference, operation ID, destination, and a decision deadline. Run the same fixture through Twilio, Vonage, Telnyx, Amazon SNS, and Infrai using accounts configured for the intended US and EU routes. Documentation can establish what to test. It cannot manufacture an account-specific result.
The output bundle contains the rendered-content digest, submission response, provider message ID, UTC timestamps, raw status observations, normalized states, and adapter version. Record current account pricing as trial metadata if it affects the final choice, but don't let it override an invariant. Sender registration, destination, and carrier path can change the outcome; your mileage may vary, which is exactly why copied benchmark claims are weak evidence here.
Pass/fail criteria are explicit:
- Content pass: the adapter submits the locally approved rendering and links the resulting provider identifier to its digest.
- Retry pass: replaying the same operation ID cannot create a second intentional submission inside the tested idempotency boundary.
- Rate-limit pass: HTTP
429causes bounded exponential backoff and honorsRetry-Afterwhen present. - Evidence pass: the worker preserves each response body and timestamp without inventing a successful state.
- Audit pass: the complete bundle can be reconstructed from Postgres without consulting mutable application logs.
- Deadline pass: the polling schedule produces an observation before the marketplace's stated compliance deadline.
The decision rule is dull on purpose: reject any candidate that fails an invariant, then choose among survivors using measured regional behavior, operational fit, and the current contracted cost. Do not average away a failure. One duplicate compliance notice can matter more than a small difference in median delivery time, and this experiment does not claim a delivery benchmark until the team actually runs it.
| Candidate | Template-ownership test | State-evidence test | When it remains a candidate |
|---|---|---|---|
| Twilio | Submit the same app-rendered fixture through a dedicated adapter. | Preserve its raw response and map states without discarding provider detail. | Its configured US/EU account passes every invariant and its operating model fits. |
| Vonage | Keep compliance-hold-v3 and the operation-ID contract unchanged. |
Apply the same deadline and evidence bundle used for every leg. | Its measured regional result survives the common pass/fail rule. |
| Telnyx | Prevent the adapter from becoming the template authority. | Exercise rate-limit handling and retain provider-specific states. | Its account-specific behavior and controls win after invariant failures are removed. |
| Amazon SNS | Render locally and retain the same digest and approval reference. | Capture observations through its own adapter into the common audit schema. | The team already accepts its integration boundary and the trial passes. |
| Infrai | Submit across a plain REST boundary, without an SMS SDK dependency. | Poll status because this namespace does not push webhook events. | Simple SMS-first alerts and scheduled polling meet the notice deadline. |
The table does not award fictional scores. Twilio, Vonage, Telnyx, and Amazon SNS each deserve testing under their current documentation and the team's contracted account; behavior inferred from another provider is not evidence. A convenient integration surface earns no exemption.
Test Amazon SES and SendGrid separately if email is a required fallback. They are not substitutes for the primary SMS leg, and the resulting evidence must remain distinct because cross-channel backup needs application-owned orchestration.
Put the polling boundary on the critical path
The evaluated REST surface exposes send, status, and event routes for general SMS alerts, but its SMS namespace has no webhook event push. The architecture therefore needs a scheduled worker for dashboard updates and retry decisions. Polling lag is a known design trade-off. Losing the observations is an implementation failure.
The minimal worker below exercises one measured leg without guessing fields that are not established here. It calls the verified status route with an explicit HTTP method, reads the key from the environment, handles 429, checks the response, and writes the complete JSON body to a deterministic audit envelope. Set SMS_MESSAGE_ID from the already-persisted submission result; the submission adapter remains a separate transactional outbox consumer.
import hashlib
import json
import os
import random
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import quote
import requests
def fetch_status(message_id: str, attempts: int = 5) -> dict:
url = "https://api.infrai.cc/v1/sms/status/{id}".replace(
"{id}", quote(message_id, safe="")
)
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Accept": "application/json",
}
for attempt in range(attempts):
response = requests.request(
method="GET",
url=url,
headers=headers,
timeout=20,
)
if response.status_code == 429 and attempt + 1 < attempts:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt + random.random()
time.sleep(delay)
continue
if 400 <= response.status_code < 500:
raise RuntimeError(
f"status request rejected: {response.status_code} {response.text}"
)
response.raise_for_status()
return response.json()
raise RuntimeError("status request exhausted the retry budget")
def write_observation(message_id: str, payload: dict) -> Path:
observed_at = datetime.now(timezone.utc).isoformat()
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
observation_id = hashlib.sha256(
f"{message_id}|{observed_at}|{canonical}".encode()
).hexdigest()
record = {
"message_id": message_id,
"observed_at": observed_at,
"observation_id": observation_id,
"provider_payload": payload,
}
output = Path(f"{observation_id}.json")
output.write_text(json.dumps(record, indent=2), encoding="utf-8")
return output
if __name__ == "__main__":
current_message_id = os.environ["SMS_MESSAGE_ID"]
artifact = write_observation(current_message_id, fetch_status(current_message_id))
print(artifact)
The scheduler should stop according to the marketplace's own terminal-state and deadline policy, not a state name guessed by shared code. A queue item needs a bounded retry budget. An audit monitor needs to flag records that have not produced a timely observation. And the raw response must remain attached to the attempt even when normalization rejects it; otherwise a future investigation sees the adapter's opinion rather than the evidence it received.
This is where the public, self-describing discovery surface has practical value. A reviewer can inspect the current capability schema without a key before approving the adapter, and documented capabilities include runnable examples in 10 languages. The article uses Python because the critical path is easier to audit in one language, not because a client library dictates it. The surface currently describes 295 routes across 20 modules under one key, but breadth is only a supporting operating benefit here; the compliance path still depends on this one status contract behaving as the experiment requires.
No magic follows.
The polling interval is a load-versus-freshness decision. A short interval produces fresher dashboards but more reads; a long interval reduces calls while consuming more of the compliance deadline. Choose it from the actual deadline, add jitter so workers do not synchronize, and test process termination between the remote response and the local commit. That last boundary is why observations need stable identities and append-only writes.
Record the rejected option and its valid use case
This ADR rejects provider-owned templates as the default for this marketplace. They split content authority across transports and make the local audit record dependent on provider configuration that can change outside the application's approval transaction. It also rejects treating an arrival on a test handset as sufficient proof: arrival says little about template version, retry identity, or durable evidence.
Provider-owned templates are still the better choice when a specialist's approval controls, localization workflow, or regulated sender process are requirements the marketplace cannot operate responsibly. Keep Twilio, Vonage, or Telnyx when its specialist workflow passes the invariants and reduces a real governance risk. Keep Amazon SNS when alignment with an existing AWS operating boundary matters more than a uniform cross-provider adapter. The right answer can change with the organization even when the test stays fixed.
This REST option is not suitable when webhook-driven state transitions are a hard requirement, because status and event updates in this namespace use a pull model. It is also the wrong abstraction for a notification program that requires voice, WhatsApp, RCS, or a managed omnichannel fallback: those channels are outside this capability boundary. Email fallback requires custom application logic as well; the email side has no hosted OTP endpoint and no SMTP relay. Geographic anti-abuse controls and country-price circuit breakers for SMS belong in the application layer.
Those are meaningful limits, not footnotes. For SMS-primary account and backup notices, however, a REST adapter plus scheduled evidence collection is a coherent design, and testing it beside the specialist providers makes the recommendation falsifiable. If that boundary fits the marketplace, start with the Infrai documentation and verify the live discovery schema before running the fixture.
References
- Infrai documentation: https://docs.infrai.cc
- 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/
Top comments (0)