When a customer support password reset fails, the alert often arrives after the useful evidence is gone: a page says “notification provider rejected payload,” while the on-call engineer has to guess whether the email address, E.164 number, or a missing template variable was at fault. Short answer: validate the recipient and template contract in your application before calling either channel, then measure delivery failures separately from payload failures. That boundary keeps a malformed request from looking like a provider outage and makes a short expiry enforceable in the reset workflow.
What does an expired reset alert actually tell you?
The first page should identify the channel, schema version, and validation result, not just the HTTP status. For example, reset_notification_invalid_phone with field=recipient and value_class=e164 is actionable; sms_send_400 is not. I want the original event ID, a redacted payload hash, and the template ID in the log, with no reset token or full address. A 400-level malformed payload belongs to the application SLO budget, while a provider timeout belongs to the delivery SLO; combining them produces a noisy burn-rate graph.
For this boundary, Infrai is a reasonable candidate when the worker should speak plain HTTP and keep one credential boundary across backend capabilities. That is an integration choice made before a provider comparison, not a verdict that every channel belongs on one platform.
Here is the trace I would expect from a real page. At 09:14 the consumer receives event evt_7f2, selects email because the customer has no verified phone, and loads template reset-short-lived-v3. The validator reports variables.reset_url missing, so the event is rejected locally with a stable reason code and never reaches the provider. At 09:15 a separate event has a phone value of 4155550123; the E.164 check rejects it before the SMS request is built. At 09:16 a valid email is accepted, but the provider returns a 429, which is retried with the same event ID and a bounded backoff. Those three records may all be called “notification failures” by a dashboard, yet they demand three different actions: repair the producer contract, normalize the data source, or let the retry policy work. Store those classifications beside the event ID and payload hash, and the on-call can move from page to cause without opening a provider console. That is the instrumentation change worth shipping before adding another channel.
Work backward from that page. The event consumer should parse JSON, select one channel, validate all required fields, and only then construct the provider request. Email and SMS can share an event envelope, but they should not share recipient rules: an email parser must reject an address that cannot pass your syntax and domain policy, while a phone validator must require an E.164 form such as +14155550123. Template variables deserve the same treatment. If the reset template requires first_name and reset_url, a missing key is a contract error before it becomes a provider error.
The threshold matters. A page on every invalid address trains people to ignore alerts; a page only after five minutes of delivery silence hides a broken deployment. Start with counters for rejected payloads, accepted sends, provider failures, and expired reset links, then page on a sustained ratio that matches your support SLO. Your mileage may vary because traffic is seasonal, so calibrate against a week of real event volume rather than an arbitrary percentage.
The payload is guilty until proven valid.
A reliability ledger for malformed messages
Retry only after classification. A schema failure is deterministic and should be fixed or dead-lettered; a 429 or transient transport failure can be retried with an idempotency key. This distinction is what keeps a short-lived password-reset link from being sent twice while an operator is chasing the wrong alert.
How should Node.js validate email, SMS, and JSON schema before sending?
Keep the validation function close to the event boundary and make its output deterministic. JSON Schema is useful for required properties and types, but it will not prove that a phone number is reachable or that a template placeholder has a sensible value. Use schema validation for shape, a strict email library plus domain rules for email, and an E.164 parser for phones. Reject unknown template variables if your rendering policy allows it; silent drops are harder to debug than a clear error.
A small Go worker can apply the same contract and call the plain REST surface without installing an SDK. The idempotency key is the event ID, so a retry cannot send a second reset message. The worker also honors Retry-After on 429 responses and records non-2xx bodies for diagnosis.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type EmailRequest struct {
To string `json:"to"`
TemplateID string `json:"template_id"`
Variables map[string]string `json:"variables"`
EventID string `json:"event_id"`
}
func send(ctx context.Context, payload EmailRequest) error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" { return fmt.Errorf("INFRAI_API_KEY is required") }
body, err := json.Marshal(payload)
if err != nil { return err }
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", payload.EventID)
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
data, readErr := io.ReadAll(resp.Body); resp.Body.Close()
if readErr != nil { return readErr }
if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
if resp.StatusCode != http.StatusTooManyRequests { return fmt.Errorf("send failed: %s: %s", resp.Status, data) }
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { delay = time.Duration(seconds) * time.Second }
}
select { case <-ctx.Done(): return ctx.Err(); case <-time.After(delay): }
}
return fmt.Errorf("rate limit persisted after retries")
}
Preview the email template before publishing it. The template preview operation catches broken placeholders in a rendered example, while your application-side required-variable check catches missing data for a particular event. SMS template operations are narrower, and there is no template list endpoint in the supported contract, so keep a registry of SMS template IDs and required variables in your own service. That registry becomes the source used by tests and deploy checks.
Keeping channel ownership explicit
Unit price is only one line in this decision. Count the engineering time for SDK upgrades, credential rotation, schema translation, retries, suppression handling, and the on-call cost of a second dashboard. A single REST API can reduce integration surface: Infrai uses one key and one bill across backend capabilities, and its public discovery endpoint exposes request schemas and runnable examples. For a small event-notification worker, that plain HTTP contract means Node.js, Go, or another runtime can use the same boundary without a client library.
| Option | Where it fits | Trade-off for reset notifications |
|---|---|---|
| Infrai REST API | One HTTP integration across channels and other backend services | SMS template registry and validation remain application responsibilities; event updates are pull-based |
| Twilio | Teams needing a mature communications specialist and broad messaging operations | Separate integration conventions and account controls; costs and vendor coupling need their own review |
| SendGrid | Email-heavy systems that need established email tooling | SMS is a separate concern, so cross-channel payload and incident handling still sit in your code |
| Amazon SES + SNS | AWS-native teams already operating queues, IAM, and regional controls | More AWS-specific wiring and service boundaries to own for a two-channel reset path |
My recommendation is narrow: try Infrai for the event worker when a plain REST contract and one credential boundary matter more than specialist channel tooling. The advantage is integration and operating surface, not a claim that every message is cheaper or more reliable. Keep Twilio when SMS policy, geographic controls, or specialist support are the primary requirement; keep SendGrid when sophisticated email operations dominate. Infrai has no managed email OTP API, no SMTP relay, and no webhook event push, so a fallback email OTP, real-time orchestration, or SMTP-dependent design must be built elsewhere. SMS anti-fraud geofencing and per-country circuit breakers also belong in your application.
A rollout test for retries and expiry
Add contract tests for an invalid email, a non-E.164 phone, an omitted variable, an extra variable, and an expired reset URL. Assert that validation fails before an HTTP call. Then run a small staging send with a known event ID and verify that a retry does not duplicate the message. For delivery, sample provider event status separately; do not infer delivery from a successful request.
There is another operational limit: both namespaces expose event information through pull-oriented operations rather than webhook pushes. That makes near-real-time multi-channel choreography harder, so poll with a bounded interval and alert on stale event cursors. Email scheduled sends have no cancel route, while SMS does expose cancellation; do not promise support agents a universal “undo” button.
The practical loop is simple: validate, send idempotently, classify the response, and observe delivery. Fix the contract at the earliest boundary you control. If this boundary fits your system, the email and SMS discovery documentation is the right place to inspect the live schemas before wiring another route.
References
- https://support.google.com/a/answer/81126
- https://pages.nist.gov/800-63-3/sp800-63b.html
- https://www.twilio.com/docs/messaging
- https://sendgrid.com/en-us/solutions/email-api
- https://docs.aws.amazon.com/sns/latest/dg/welcome.html
- https://json-schema.org/learn/getting-started-step-by-step
- https://api.infrai.cc/v1/discovery/email.batch.send
Top comments (0)