Short answer: I chose an API with scheduled SMS cancellation and polling because health reminders can become wrong between scheduling and delivery; I kept template ownership in the application so every change remains reviewable.
That decision is narrower than “which messaging vendor is best?” A vaccination reminder, a lab pickup notice, and a medication alert have different expiry rules. In our backend, the message body is a versioned template, while the delivery service owns transport, status, and cancellation. That boundary makes an audit trail possible without pretending the provider understands our clinical workflow.
The experiment: cancellation mattered more than another template editor
The simple design was to schedule a message and forget it. It looked tidy in a notebook, then fell apart when an appointment moved. A stale reminder is not merely noisy; it can send a patient to the wrong place. I wanted a cancel operation tied to the delivery id, plus a way to check what happened after the send.
The trade-off was template ownership. Provider-hosted templates can help a marketing team move quickly, but they add a second review system for regulated copy. Keeping templates in Python means our pull request contains the wording, locale, and expiry logic. It also means we own rendering tests and character-count checks. That is work, but it is visible work.
I started with a 15-minute reminder window and a 24-hour follow-up. If the appointment was canceled, the job called the SMS cancel route before the send time. After sending, a worker polled status and events, recording the delivery id and the last observed state. Short loops are easier to reason about.
The rule is simple.
In the longer test, a clinic moved 38 appointments during one afternoon. Each reminder carried our internal appointment id, locale, and an expiry timestamp. The scheduler created one delivery record per id, and the cancellation path marked that record before touching the provider. A reconciliation worker then compared the local state with the status response, while the events response supplied the latest delivery transitions. When a message was already sent, cancellation was no longer a business option, so the worker logged that fact and let the fallback policy decide whether email was appropriate. That separation kept a late carrier update from rewriting the audit record. It also exposed the real cost of polling: more reads, more queue work, and a delay between an event and an escalation. We measured those effects instead of assuming a vendor dashboard would answer them.
No shortcut.
How should a healthtech backend handle scheduled SMS alerts and reminders?
Treat scheduling as a state machine in your app. scheduled can become canceled, sent, delivered, or failed; only your business rules know which transitions are still acceptable. Polling is deliberate here: status confirms the current delivery state, while events provide a sequence you can use for fallback logic.
Here is a compact Python client. It uses the documented SMS routes, an explicit method, bearer auth, and bounded exponential backoff for rate limits. The idempotency key is derived from our reminder id so a retry does not create a second alert.
import hashlib
import os
import time
from typing import Any
import requests
BASE_URL = os.environ["SMS_API_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def request_json(method: str, path: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
for attempt in range(4):
response = requests.request(method, BASE_URL + path, json=payload, headers=headers, timeout=10)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(f"SMS request failed ({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
time.sleep(min(delay, 30))
raise RuntimeError("SMS request stayed rate-limited after retries")
def schedule_alert(reminder_id: str, to: str, body: str) -> dict[str, Any]:
key = hashlib.sha256(reminder_id.encode("utf-8")).hexdigest()
return request_json(
"POST",
"/sms/send",
{"to": to, "body": body, "idempotency_key": key},
)
def cancel_alert(message_id: str) -> dict[str, Any]:
return request_json("POST", f"/sms/cancel/{message_id}")
def read_delivery(message_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
status = request_json("GET", f"/sms/status/{message_id}")
events = request_json("GET", f"/sms/events/{message_id}")
return status, events
The request body shape is intentionally small; use the discovery schema to validate the exact fields your account exposes before shipping. I would measure cancellation success, time from send to first status, duplicate-send rate, and fallback latency. Those metrics tell you whether polling meets the product's deadline. I'm not sure a 30-second poll interval is right for every clinic; your mileage will vary with carrier behavior and escalation policy.
What do the main SMS API options trade away?
The provider decision still matters, especially for sender registration, regional coverage, and ownership of message content. I compare capabilities rather than sticker prices because per-country rates and compliance requirements change.
| Option | Where it fits | Trade-off for this workflow |
|---|---|---|
| Twilio Messaging | Broad ecosystem, mature messaging tools, and many integration examples | More product surface to govern; template and compliance choices can live across console and code |
| Vonage Messages/SMS | Teams already using Vonage communications and its account model | Cross-channel orchestration can add another abstraction layer to test |
| Amazon SNS SMS | AWS-native systems that want IAM, queues, and regional controls nearby | Application teams often assemble scheduling, cancellation policy, and delivery polling themselves |
| Infrai | A self-describing REST surface with runnable examples, plus SMS cancel, status, and event routes under one key | Events are polling-only; geographic throttles and spend guardrails remain application responsibilities |
The last row is a fit when reducing SDK-specific glue is more valuable than adopting a provider-owned template workflow. Its discovery endpoint documents request and response schemas with runnable examples, so wiring a new capability is reading one endpoint rather than learning another SDK. The same key and billing boundary can also cover adjacent backend capabilities, which keeps this particular service small.
Where this choice is not suitable
The catch is real-time, cross-channel escalation. Neither namespace pushes webhook events, so switching from SMS to email immediately after a carrier event requires polling latency. Build a queue and an explicit deadline if that delay is acceptable; choose a webhook-first messaging design when it is not.
This option is also a poor match if you need hosted email OTP, SMTP relay, voice, WhatsApp, or RCS. Email scheduling does not provide the same cancellation control described above. Abuse prevention, including per-country throttles, geofencing, and a pricing circuit breaker, belongs in the application. Domestic email vendor readiness should not be treated as a compliance guarantee.
Stick with Twilio, Vonage, or SNS when your organization already has sender registration, support, and observability deeply embedded there. Switching only makes sense after an evaluation harness shows lower operational effort without weakening delivery audits.
Top comments (0)