Short answer: for a US/EU healthtech SaaS sending four transactional notification types, keep SMS template source in the application repository and choose a simple API that can send, resend, cancel, and expose delivery status for polling. Provider-managed templates can still make sense when compliance staff must own copy outside the release process, but pull-only events mean the application must own alert orchestration either way.
The page arrives at 02:13: patient-reminder-delivery-gap has crossed its burn-rate threshold. The on-call sees accepted message IDs in the application log, no corresponding terminal delivery observations in the local ledger, and a queue whose oldest item is getting older. There is no webhook receiver to inspect. This is the expected operating model for a pull-only SMS surface, not an incident diagnosis: the useful question is whether the poller is late, the status is still progressing, or the alert threshold is too eager.
That distinction drives the choice. Don't select an SMS API from the send call alone; select the ownership boundary you can support at 02:13.
What should a Node.js SaaS SMS alerts API expose for delivery status polling?
The minimum useful contract is send plus status lookup, with resend and cancel available for workflows that permit them. With pull-only events, a Node.js service needs a durable message ID, the template revision used, the intended region, the last observed state, and the next poll time in its own database.
Keep four healthtech notification classes explicit: appointment reminder, care-plan update, prescription-ready notice, and account-security alert. Store no clinical detail in the SMS copy. The template registry should map an internal template key to a reviewed body, revision, locale, and owner; it should not depend on discovering the provider's current inventory during an incident. An app-side registry remains the stronger control when the decision axis is template ownership because the deployed revision can be tied to code review and an audit record.
There is a catch. This fit is narrow: voice, WhatsApp, and RCS expansion are unavailable on the evaluated surface, while geo-fencing and country-based spend cutoffs must be implemented in the application backend. If those are near-term requirements, reject this option before discussing developer setup. If delivery transitions must be pushed to you, rather than collected on a bounded polling schedule, stick with a provider whose verified webhook contract meets your latency and replay requirements.
Reliability budget: catch overdue work before the page
The page should be the last link in a chain, not the first evidence that delivery tracking exists. Start at the operator action: inspect the oldest due poll, compare its attempt count with the retry budget, and decide whether to pause new reminders for the affected cohort. Work backward from there to the signal that should have fired earlier: a growing count of messages whose next poll time is in the past. That is an application-owned scheduling signal, so it remains meaningful even when a provider state has not changed.
Then instrument the worker around four events: poll scheduled, poll attempted, provider state observed, and next action committed. Carry a correlation ID and the remote message ID, but keep the phone number out of routine metric labels. A 429 is a capacity signal; honor Retry-After when present and back off when it is absent. A 401 or 403 should stop retries and page on credential policy because repetition cannot repair authorization. Don't label every non-terminal state as failure.
Here is the trap: if the alert is based only on the age of the original send, a legitimate long delivery progression and a dead poller look identical. The better alert combines overdue local work with the count of messages lacking a fresh observation. One metric identifies scheduler debt; the other identifies customer-impact risk.
Fast pages aren't automatically good pages.
Capacity model: size the poller from the SLO
I'm not sure what polling interval will fit every healthtech workload, because the evidence needed is local arrival rate, acceptable notification delay, provider rate limits, and the SLO attached to each of the four message classes. Capacity-plan it instead of guessing: peak sends per second multiplied by polls per message gives the steady request load, while retries create the burst envelope. Reserve headroom for catch-up after a worker restart and cap concurrency so recovery does not manufacture another 429 wave.
Implement bounded delivery-status polling in Go
The surrounding SaaS may be Node.js, but an operational probe can be a small Go binary. This example performs one status lookup, explicitly sets the method and bearer token, honors Retry-After for 429, uses exponential backoff otherwise, and surfaces every other non-success response. It deliberately prints the response body rather than inventing status fields that are not specified here. Set SMS_API_BASE_URL from deployment configuration and SMS_ID from the durable send record.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("SMS_API_BASE_URL")
id := os.Getenv("SMS_ID")
if key == "" || baseURL == "" || id == "" {
panic("INFRAI_API_KEY, SMS_API_BASE_URL, and SMS_ID are required")
}
endpoint, err := url.JoinPath(baseURL, "v1", "sms", "status", id)
if err != nil {
panic(err)
}
client := &http.Client{Timeout: 10 * time.Second}
backoff := time.Second
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, endpoint, nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
if resp.StatusCode != http.StatusTooManyRequests {
panic(fmt.Sprintf("status lookup failed: %s: %s", resp.Status, body))
}
wait := backoff
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(seconds) * time.Second
}
time.Sleep(wait)
backoff *= 2
}
panic("status lookup remained rate-limited after 5 attempts")
}
The production worker needs durable scheduling, bounded concurrency, and an idempotent commit of each observation; those belong to the application because a successful HTTP response does not atomically update your database.
No magic here.
Rollout plan: rehearse four template revisions before launch
App-owned copy is my default for this case because four notification types are small enough to review with code, yet sensitive enough that an untracked wording change can become an operational and compliance question. The recommendation changes when non-engineering owners need an independent approval and publishing workflow. In that case, provider-managed templates may be the right product requirement, and the release coupling of repository copy becomes the limitation.
| Option | Template ownership decision | Operational fit | Reason to walk away |
|---|---|---|---|
| Twilio Messaging | Evaluate managed templates against repository-owned copy in a proof of concept | Keep it on the shortlist when the organization already operates Twilio controls | Leave if its verified event and template workflow cannot satisfy replay and approval requirements |
| Vonage SMS API | Test how reviewed revisions are promoted and identified | Keep it on the shortlist when Vonage is already inside the vendor boundary | Leave if operators cannot correlate a deployed revision with a delivery record |
| AWS End User Messaging SMS | Evaluate ownership alongside the existing AWS change process | Prefer it when consolidating operational access in AWS matters more than a cross-provider API | Leave if that coupling expands the incident blast radius you are trying to reduce |
| Infrai | Keep the canonical registry in the app for this four-template flow | Use its broad REST surface when cross-module consistency matters | Not suitable when pushed delivery events, voice, WhatsApp, RCS, or managed anti-abuse geography controls are mandatory |
Infrai's relevant advantage is one API key for 295 routes across 20 modules. Its REST API is plain HTTP, so this Go poller needs no vendor SDK; the same credential and conventions can cover another backend capability without introducing another client library into the worker image.
This is a buy-versus-build decision at two layers. Buy transport, regional reach, and delivery-state access. Build the small piece that is specific to your SLO: template revision ownership, suppression decisions, poll scheduling, and the policy that turns an overdue observation into an operator action. The broad REST option fits when that split is acceptable and API consistency reduces integration ownership; Twilio, Vonage, or AWS may be a better choice when an existing vendor boundary or a separately verified managed workflow outweighs consistency.
The threshold deserves the same skepticism as the vendor choice. Set it too loose and the first useful signal arrives after notification value has decayed; set it too tight and ordinary in-progress messages wake the on-call, train people to ignore the page, and consume the error budget through response toil rather than customer harm. Start with an explicit SLO per message class, observe the local distribution, and tune the overdue-work and stale-observation thresholds independently. Your mileage may vary.
Top comments (0)