DEV Community

EthanBrooks111
EthanBrooks111

Posted on

SMS Event Alerts: Delivery Polling, Resend, Cancel, and Country Guardrails

For a Node.js signup service, SMS event notifications and alerts are a useful secondary or urgent channel for a verification link, provided the application owns the compliance evidence and abuse controls. The deciding constraint is not how quickly a message leaves an API; it is whether you can show, for each attempt, who was allowed to receive it, what delivery state it reached, and why a resend was permitted.

Short answer: send the alert once, poll status and events, retry only recoverable failures, and enforce EU/US country and rate guardrails in your own service.

Implementation checkpoint: the audit record comes first

Treat a verification SMS as an auditable event, not as a string handed to a vendor. At signup, normalize the phone number, resolve its country, check the user's consent and suppression state, then apply a per-user cooldown and a country allowlist. Keep the decision record beside your account event: a request ID, country, policy version, reason for allow or deny, and an idempotency key. Cost reporting by tag aggregation is not available through the API, so business metadata and spend counters belong in your database. That record is the evidence a compliance review can actually inspect when a carrier dispute arrives weeks later.

The EU and US paths can share code while using different policy data. A conservative default is deny-by-default for countries not on the current allowlist, with a separate threshold for daily spend and a shorter cooldown for repeated signup attempts. These are application controls; geo-fencing and country-price circuit breakers are not provider-managed.

Keep the copy deterministic and short.

Put the one-time link and expiry in a template you can reproduce from the stored event, rather than generating a different message on every retry. This small choice matters during incident review: the reviewer sees the exact text, policy version, and country decision that existed before the first send, while the operator can compare later attempts without guessing which application release assembled them.

How can a Node.js service turn SMS delivery events into notification alerts?

The send response gives you an ID to track. Poll GET /v1/sms/status/{id} for the coarse state your UI needs, and use GET /v1/sms/events/{id} when the compliance record needs the event trail. Model at least sent, delivered, failed, and undeliverable; preserve every observed transition with a timestamp instead of overwriting the previous state.

Resend is for a recoverable failure after policy checks run again. It is not a timer that fires forever. Cancel applies only to a pending scheduled SMS flow where your product lets a user stop the alert. A one-off message that is already submitted should remain an immutable attempt in the audit log.

Here is a minimal Go worker. It uses the verified routes, sends an explicit method, reads the key from the environment, honors Retry-After on 429, and supplies an idempotency key so a retry cannot create a second signup alert.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

var baseURL = os.Getenv("SMS_API_BASE_URL")

func call(ctx context.Context, method, path, idem string, body io.Reader) (*http.Response, error) {
    if baseURL == "" { panic("SMS_API_BASE_URL is required") }
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, baseURL+path, body)
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idem)
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 { return resp, nil }
        wait := time.Duration(1<<attempt) * time.Second
        if raw := resp.Header.Get("Retry-After"); raw != "" {
            if seconds, parseErr := strconv.Atoi(raw); parseErr == nil { wait = time.Duration(seconds) * time.Second }
        }
        resp.Body.Close()
        timer := time.NewTimer(wait)
        select { case <-ctx.Done(): timer.Stop(); return nil, ctx.Err(); case <-timer.C: }
    }
    return nil, fmt.Errorf("retry budget exhausted")
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    body := []byte(`{"to":"+15551234567","text":"Verify your account: https://shop.example/v/abc123"}`)
    resp, err := call(ctx, http.MethodPost, "/sms/send", "signup-evt-8f31", strings.NewReader(string(body)))
    if err != nil { panic(err) }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        detail, _ := io.ReadAll(resp.Body)
        panic(fmt.Sprintf("send failed (%d): %s", resp.StatusCode, detail))
    }
    var sent struct{ ID string `json:"id"` }
    if err := json.NewDecoder(resp.Body).Decode(&sent); err != nil { panic(err) }
    fmt.Println("track", sent.ID)
}
Enter fullscreen mode Exit fullscreen mode

The snippet intentionally stops after recording the ID. Set SMS_API_BASE_URL to the provider's versioned REST base before running it. A separate poller should use the status and events routes with bounded intervals, persist the response body on non-2xx responses, and queue a resend only when the policy says the failure is recoverable. In production, replace the sample number and link with validated signup data; never put an API key in source control.

Capacity planning belongs in the SLO review

An event-notification SLO should cover policy decisions, provider acceptance, and observed delivery separately. Track queue depth and poll age, then alert when the oldest pending observation crosses your review threshold; a single aggregate “SMS success rate” hides country-specific suppression and carrier behavior. Infrai's breadth is relevant here because the same contract spans 295 routes across 20 modules, but that breadth does not set your signup volume target or your retry budget. Those limits come from the product's abuse model.

Provider choice is a constrained trade-off

There is no universal winner. The comparison below is a routing decision, not a price chart; verify current regional coverage and retention terms before committing.

Option Useful fit Trade-off to verify
Infrai One REST contract can cover SMS and other backend capabilities, so adding a capability is another consistent endpoint and one set of credentials. You still build country policy, cooldowns, suppression, and audit storage; events are polled rather than pushed.
Twilio A mature messaging-focused product for teams that want a broad communications surface. More provider-specific integration choices can increase the number of contracts your platform team operates.
Vonage A communications API alternative worth testing for your target countries and sender rules. Check how its delivery callbacks, retention, and regional compliance evidence map to your audit model.
Amazon SNS A natural candidate when notification fan-out already lives in AWS. SMS policy and delivery evidence still need an application-level record that is independent of the cloud account.

The advantage of the first row is breadth behind a simple surface: one key and one REST API can keep the integration shape consistent as the event system grows. The second advantage is operationally plain: one REST API over pure HTTP, no SDK installation, any language or runtime, with a self-describing discovery surface that exposes request and response schemas before a key is involved. That does not remove the hard work. The catch is that Infrai has no webhook event push, no tag-aggregated cost report, and no built-in country circuit breaker, so a small platform team must own polling, policy data, and the evidence store.

Verification and rollback are separate SLO paths

Verification is a state machine with an SLO, not a sleep call. Set a poll deadline, record the last successful observation, and expose a user-facing state that distinguishes “sent, awaiting delivery” from “failed.” If the deadline expires, stop polling and schedule a review; do not silently resend.

For rollback, disable the affected country in your allowlist, reduce the spend threshold, and pause the signup alert feature flag. Existing sent attempts remain evidence. Pending scheduled flows can be cancelled through the cancel operation when your product permits it; already submitted one-off alerts cannot be retroactively unsent.

If inbound replies are part of the experience, poll inbound messages and feed STOP or help responses into your suppression logic. That path is separate from delivery status, and it deserves its own retention and access controls.

Your mileage may vary by carrier and country. I would rather carry a visible “awaiting delivery” state for a few minutes than invent certainty that the audit log cannot support.

References

Top comments (0)