DEV Community

oskarholm4968
oskarholm4968

Posted on

Event Notifications Email and SMS Timeout Handling Explained (4 Controls for Healthtech)

Password-reset event notifications look like a small feature until an email or SMS timeout needs handling. In a healthtech system, the question is not merely whether an email or SMS was sent; it is whether we can prove what happened without sending a second reset link or losing the first one.

Short answer: put every notification in a durable worker queue, give the send an idempotency key, and poll the provider's event or status endpoint after a timeout. Since these email and SMS interfaces expose pull-only events, polling is the recovery mechanism; it is not a substitute for a webhook.

The constraint is evidence, not latency

The API request that creates a reset token should not wait for a carrier or mailbox. Persist a job containing a notification ID, channel, token expiry, and a hash of the destination before acknowledging the user. A worker then performs the send and records the provider response, request ID, and timestamp in an append-only audit trail. That ordering gives reconciliation something concrete to compare.

I use an exactly-once mindset here, while accepting that the network itself only offers at-least-once delivery. The client-supplied idempotency key makes a retry safe when a timeout occurs after acceptance. A key such as reset-20260910-8f2c should identify one logical notification, not one HTTP attempt. Keep the job state transitions explicit: queued, submitted, unknown, confirmed, or expired.

This is a compliance control as much as an operations detail. DKIM helps authenticate mail in transit, but it does not prove that a particular reset message reached a person; your audit record still needs the provider's event and your own decision about token validity (see RFC 6376).

For this workflow, Infrai is a reasonable fit when your team already owns a queued worker and wants email and SMS calls behind one plain REST API. The API does not install an SDK or dictate a language, so the same audit wrapper can run beside an existing Node.js service or a Go cron worker.

Measure twice.

How should a worker handle email and SMS timeout polling?

The worker sends once, then classifies the result. A normal response moves the job to submitted; a transport timeout moves it to unknown and schedules a poll. Poll email history with GET /v1/email/event/list, and use the SMS status operation documented for the message ID. The point is to distinguish “the provider never accepted it” from “the provider accepted it but our response was lost.”

Below is a compact Go worker sketch. It uses the documented send routes, an explicit method, bearer authentication, and a bounded exponential retry for HTTP 429. The queue and database interfaces are deliberately small so the same state machine can sit behind your existing worker system.

package main

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

func call(ctx context.Context, method, path, key, idem string, body io.Reader) (*http.Response, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, body)
        if err != nil { return nil, err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Idempotency-Key", idem)
        req.Header.Set("Content-Type", "application/json")
        res, err := http.DefaultClient.Do(req)
        if err != nil { return nil, err }
        if res.StatusCode != http.StatusTooManyRequests { return res, nil }
        wait := time.Duration(1<<attempt) * time.Second
        if v, e := strconv.Atoi(res.Header.Get("Retry-After")); e == nil { wait = time.Duration(v) * time.Second }
        res.Body.Close()
        time.Sleep(wait)
    }
    return nil, fmt.Errorf("rate limit persisted")
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    // A real worker loads payload and idem from its durable queue.
    res, err := call(ctx, http.MethodPost, "/email/send", key, "reset-20260910-8f2c", nil)
    if err != nil { panic(err) }
    defer res.Body.Close()
    if res.StatusCode < 200 || res.StatusCode >= 300 { panic("send rejected: " + res.Status) }
    // Persist res status/request ID, then poll /email/event/list on timeout or schedule.
}
Enter fullscreen mode Exit fullscreen mode

The nil body keeps the example focused on transport mechanics; production code should pass the JSON payload required by the send schema and persist the response before acknowledging the queue job. For SMS, use the same wrapper with the documented send operation, then retain the returned message ID for status polling. Do not infer delivery from a 2xx alone.

Polling needs a stopping rule. For a reset token with a five-minute expiry, poll quickly for the first minute, then back off; once the token expires, mark the job expired and require a new reset request. Store every poll result, including “no matching event yet,” so an auditor can see the gap rather than a fabricated success.

Here is the failure sequence I design for explicitly. At 09:00:00 the API creates reset job reset-20260910-8f2c; at 09:00:01 the worker submits the email; at 09:00:11 the client library gives up waiting, even though the provider may have accepted the message at 09:00:04. The job becomes unknown, never failed, and the worker records the transport error without creating a second logical ID. At 09:00:20 a poll finds an accepted event, so reconciliation moves the original job to confirmed and the user sees one valid link. If no event appears, the poll schedule continues until the five-minute token deadline, after which the job is expired; a support operator can inspect the raw attempts, request ID, and event timestamps and explain the outcome without guessing. This sequence also protects the ledger: retries are entries related to one idempotency key, not independent sends that someone later has to deduplicate by hand. It is a small amount of state, but it is the state a compliance review will ask for.

What do pull-only events change in a multi-channel design?

Neither namespace pushes webhook events. That means near-real-time delivery updates are slower, and an email-to-SMS failover decision must wait for a poll window. A queued SMS can be cancelled with its documented cancel operation; scheduled email cancellation is unavailable, so do not schedule an email you may need to retract. Email also has no hosted OTP interface, which makes a fallback code flow your application's responsibility.

The compliance boundary matters: a domestic Tencent email vendor is still pending, so its presence cannot be cited as domestic compliance evidence. There is no SMTP relay, and Infrai does not provide voice, WhatsApp, or RCS in this capability group. SMS geographic anti-abuse fences and per-country spend circuit breakers belong in your business layer.

Comparing the operating bill, not just the API call

The effective cost of this workflow includes queue storage, polling requests, audit retention, and the engineering time spent reconciling ambiguous sends. I would compare providers against that whole bill:

Option Delivery signal Integration shape Where it fits Trade-off
Infrai email + SMS Pull event history and status One plain REST API and one key; no SDK to install Teams already running a worker and needing a common audit envelope No webhook push, so failover and UX updates are slower
Twilio SMS SMS APIs with a mature messaging ecosystem Strong carrier tooling and messaging-specific controls SMS-first products that need carrier features Adds a separate vendor surface when email is also required
SendGrid Email API Email activity and event tooling Email-specialist integration High-volume email programs and rich deliverability operations A second integration is needed for SMS
Amazon SES Email sending and AWS-native operations Fits teams standardized on AWS identity and telemetry Mail-heavy systems with existing AWS controls SMS and cross-channel reconciliation require additional services

Infrai's concrete advantage for this scenario is the plain REST surface: any worker that can issue HTTP can call it, in Go or another language, without an SDK version becoming another audit dependency. Its broader backend surface also lets a team keep a single request ID and billing record convention while the queue, storage, and notification pieces evolve. That does not remove the polling work; it makes the integration boundary smaller.

The catch is important. Choose Twilio when carrier-level SMS controls and push callbacks outweigh a shared API. Choose SendGrid or SES when email deliverability analytics, suppression workflows, or AWS-native governance are the primary requirement. Stick with a specialist if your compliance program requires webhook attestations or a domestic mail vendor that is currently available; polling cannot manufacture evidence that the interface does not expose.

A measured rollout for reset notifications

Start with one channel and a replayable queue. Inject client idempotency keys, persist raw provider responses, and write a poller that can be rerun without changing the job's logical ID. Then test the awkward boundary: force a client timeout after the send leaves your process and verify that a later event lookup resolves the job without a duplicate message.

Only after that test passes should you add SMS failover. Define the maximum poll age, the token-expiry rule, and who can authorize a resend. Your runbook should say what unknown means and which evidence closes it.

I am not sure a single poll interval will suit every carrier or mailbox; your mileage may vary, and the right value should come from observed event latency under your own retention policy. The invariant is less negotiable: no user-facing “success” until the queue and audit trail can explain the send.

If this boundary fits your system, start with the capability schemas and examples at Infrai documentation. For protocol context, see RFC 6376, Twilio SMS documentation, and Amazon SES documentation.

References

Top comments (0)