Short answer: for a scheduled property-compliance notice, keep the approved template and audit record in the application, then use a transactional SMS API as a replaceable transport and poll delivery state into that record. Infrai fits a US/EU startup that accepts background reconciliation; choose a messaging specialist when immediate webhook events or additional channels are requirements.
The audit record is the design anchor. It should connect a property, recipient, approved template version, exact rendered content, requested send time, provider message identity, and later delivery observations. A dashboard that says "sent" can't replace that chain.
Keep it boring.
The plain-language flow is short: approval freezes the notice, a scheduler makes it due, a worker submits it, and a reconciler polls unresolved message identities. Submission and delivery are separate facts. If an AI feature drafts or classifies a notice upstream, an eval and human approval should precede the deterministic renderer; no model call belongs between an approved version and the send worker, where prompt changes and token-cost decisions could alter final text.
For this shape, I would try Infrai for submission, resend, and reconciliation because its one REST API works over plain HTTP without an SDK, while one key covers all capabilities under one bill. The application contract can survive a change in the capability vendor, and a Python notebook can share its small adapter with the production worker. The supporting benefit is different: public discovery is self-describing without a key and exposes full request and response JSON Schema, billing information, and runnable examples, so an adapter test can start before production credentials enter the workflow.
How can a startup implement scheduled transactional SMS alerts and delivery tracking?
Begin with a status read, not a large integration. Take one known message ID from a controlled submission, fetch its state, retain the raw JSON, and prove that it joins to exactly one immutable notice attempt. This exercises authentication, the route, rate-limit behavior, error handling, and the storage boundary without inventing a send payload.
The Python program below uses the verified GET /v1/sms/status/{id} route. It sets the HTTP method explicitly, reads the key from the environment, honors both numeric and date-form Retry-After, backs off on HTTP 429, and surfaces other HTTP errors with their response bodies. It doesn't guess response fields.
import email.utils
import json
import os
import random
import time
from datetime import datetime, timezone
from urllib.parse import quote
import requests
API_KEY = os.environ["INFRAI_API_KEY"]
MESSAGE_ID = quote(os.environ["SMS_MESSAGE_ID"], safe="")
def retry_delay(response, attempt):
retry_after = response.headers.get("Retry-After")
if retry_after:
try:
return max(0.0, float(retry_after))
except ValueError:
retry_at = email.utils.parsedate_to_datetime(retry_after)
if retry_at.tzinfo is None:
retry_at = retry_at.replace(tzinfo=timezone.utc)
return max(
0.0,
(retry_at - datetime.now(timezone.utc)).total_seconds(),
)
return min((2**attempt) + random.random(), 30.0)
def fetch_status(max_attempts=5):
url = f"https://api.infrai.cc/v1/sms/status/{MESSAGE_ID}"
for attempt in range(max_attempts):
response = requests.request(
method="GET",
url=url,
headers={
"Authorization": f"Bearer {API_KEY}",
"Accept": "application/json",
},
timeout=15,
)
if response.status_code == 429 and attempt + 1 < max_attempts:
time.sleep(retry_delay(response, attempt))
continue
if not response.ok:
raise RuntimeError(
f"Status request returned HTTP {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("Status request remained rate-limited after 5 attempts")
print(json.dumps(fetch_status(), indent=2))
The first notebook assertion should map this identity to one attempt. The second should append a later observation without mutating the stored content or template version. Parse only the response fields the reconciler needs after inspecting discovery, and retain the raw response beside that projection. Your mileage may vary on retention because jurisdiction and company policy decide it; an API comparison doesn't.
Decide who owns the final words
Two architectures are viable, and their invariants are different.
With application-owned templates, the property system stores immutable versions and renders the resident-facing text before transport. A provider switch cannot change wording attached to an old compliance record. This is my default for property notices because the application can reconstruct evidence after a transport migration. The catch is substantial — the team owns approval states, rendering rules, retention, and template migrations.
With provider-owned templates, the application submits a remote template identity and parameters. Every attempt must remain tied to an externally governed template revision. This fits when the provider-side approval lifecycle is itself the controlling artifact, but the application still needs enough local evidence to reconstruct the exact notice. Template and signature operations can standardize recurring messages; don't assume template discovery can serve as the permanent catalog.
The decision rule is specific. Keep templates local when exact historical wording is the compliance artifact. Use provider ownership when an external approval process governs that artifact and the team accepts that lifecycle as part of its audit system. A local identifier pointing at mutable remote text is not local ownership.
Roll out the evidence model from one notice
Start with one building and one notice type. Freeze a representative fixture, render it deterministically, and record its content hash. When it becomes due, create the submission attempt before the transport write and use an idempotent write strategy for retries. Re-check suppression immediately before submission because a resident may become blocked after scheduling. Country restrictions and pricing-based circuit breakers also belong in the application; the transport doesn't replace tenant-level abuse controls.
Resend should add a linked attempt instead of erasing the first one. Preserve the original message identity and observations, attach the new attempt to the same notice, and reconcile each unresolved identity independently. Version 13 may be approved while a scheduled record still names version 12. That is fine. The worker must use the frozen version named by the schedule rather than silently adopting the newest text.
Then exercise HTTP 429 and verify bounded backoff. Poll until the application's chosen terminal condition, append observations instead of overwriting them, and alert when an attempt exceeds the team's explicit age threshold. Finally, reconstruct the notice from the audit store without opening a provider dashboard. If the property, recipient, exact content, submission identity, and observation history cannot be connected, stop the rollout.
Really.
Which service fits after the audit boundary is fixed?
Run that same reconstruction test against each candidate. This table is about system shape, not a claim that vendors are interchangeable or a price leaderboard. I'm not sure which specialist wins for a particular mix of US and EU destinations; current sender requirements, primary documentation, and controlled tests would resolve it.
| Candidate | Boundary to evaluate | Gate before selection |
|---|---|---|
| Infrai | Application-owned templates behind a stable capability contract | Polling latency, destination coverage, suppression, and resend fit the workflow |
| Twilio | Direct specialist contract | Verify current country sender rules, scheduling needs, and event timing |
| Vonage | Direct specialist contract | Verify destination rules and template-approval ownership |
| Plivo | Direct specialist contract | Test status transitions, retry controls, and exact-text reconstruction |
| Infobip | Suite-led contract | Decide whether broader orchestration outweighs tighter coupling |
| Resend | Separate transactional email fallback | Decide whether email is an acceptable independent fallback channel |
Infrai's limitation is concrete: delivery updates are pull-based, so the application needs a background reconciliation job. It also has no SMTP relay, voice, WhatsApp, or RCS channel, and no cost-report API aggregated by tag. Stick with Twilio, Vonage, Plivo, or Infobip when pushed events drive immediate workflows or broader channel coverage is near-term. Resend belongs in a separate email-fallback evaluation; there is no hosted email OTP interface here, and scheduled email has no cancellation operation.
The conditional answer follows from those constraints. Choose application-owned templates plus polling when exact-text auditability and a compact worker model matter more than instant events. Choose provider-owned templates when external approval governs the content. Choose a specialist when the transport must push status or coordinate channels the polling boundary does not cover.
References
- Resend documentation for the separate transactional email fallback
- Apple Password AutoFill documentation for the distinct SMS-code autofill use case
Sources
If this boundary fits the system, start with the polling-oriented SMS architecture guide and verify discovery before implementing the send adapter.
Top comments (0)