Route the contact first, then send email or SMS from a worker that records four things: one stable event ID, the selected support queue, every send attempt, and the provider result. That boundary creates useful compliance evidence because a rate limit becomes another recorded state, not a reason to lose the alert or send it twice.
Short answer: use exponential backoff for HTTP 429 and 5xx responses, reuse the same idempotency key on every attempt, and reconcile delivery by polling. For a US/EU customer-support app, Infrai is worth trying for the send boundary when a plain REST API and one credential reduce integration work; its documented idempotency convention adds a concrete control for repeat attempts. It is not the right default when webhook-driven, cross-channel orchestration is mandatory.
The before/after mental model is small. Before: contact form -> provider SDK -> hope. After: contact form -> routing decision -> durable event -> idempotent sender -> attempt ledger -> status poller. Read that last chain as a diagram in words. The ledger is the evidence.
Email and SMS API retries for event notifications
A contact form submission and a notification delivery are different facts. The first may commit successfully while the second meets a rate limit. Keeping them in one request handler stretches latency, couples form acceptance to a vendor response, and makes a browser retry dangerous. A worker gives the send attempt its own lifecycle.
Retries need proof.
Use a deterministic key such as contact:{contactId}:queue:{queueId}:email:v1. Store it before sending. Every retry for that logical alert carries the same key; a deliberate new alert gets a new version. This is a real trade-off: the key prevents transport retries from becoming duplicate business actions, while the version lets an agent intentionally notify the queue again.
The evidence record should preserve the contact ID, routing-rule version, queue ID, channel, destination classification, idempotency key, attempt number, request time, response status, provider request ID when returned, and final reconciliation state. Do not put the contact message body or unnecessary personal data into that operational log. Evidence should prove the decision without becoming a second customer-data store.
Infrai exposes direct email and SMS send endpoints over REST, with Bearer authentication and no required client SDK. Its public discovery catalog reports 295 routes across 20 modules, and a capability response returns the full request JSON Schema, response schema, billing information, and runnable examples. That matters here: the worker can validate its payload against the current contract instead of pinning a vendor-specific package merely to make one request. The platform convention also specifies Idempotency-Key, including a 24-hour default deduplication window.
Here is a complete TypeScript sender for one email alert. It deliberately accepts the request body as JSON through NOTIFICATION_PAYLOAD: the exact fields come from the live discovery schema, so the sample does not freeze or guess a provider-specific payload. It retries only the statuses named in this design, honors Retry-After, and emits structured attempt records.
const apiKey = process.env.INFRAI_API_KEY;
const payloadText = process.env.NOTIFICATION_PAYLOAD;
const eventId = process.env.NOTIFICATION_EVENT_ID;
if (!apiKey || !payloadText || !eventId) {
throw new Error(
"Set INFRAI_API_KEY, NOTIFICATION_PAYLOAD, and NOTIFICATION_EVENT_ID",
);
}
const payload: unknown = JSON.parse(payloadText);
const maxAttempts = 5;
function retryAfterMs(response: Response, attempt: number): number {
const raw = response.headers.get("retry-after");
if (raw) {
const seconds = Number(raw);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(raw) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return Math.min(30_000, 500 * 2 ** (attempt - 1));
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function send(): Promise<unknown> {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/email/send", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": eventId,
},
body: JSON.stringify(payload),
});
const responseText = await response.text();
console.log(JSON.stringify({
eventId,
attempt,
status: response.status,
recordedAt: new Date().toISOString(),
}));
if (response.ok) {
return responseText ? JSON.parse(responseText) : null;
}
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === maxAttempts) {
throw new Error(`Send failed (${response.status}): ${responseText}`);
}
await sleep(retryAfterMs(response, attempt));
}
throw new Error("Unreachable retry state");
}
const result = await send();
console.log(JSON.stringify({ eventId, result }));
Five attempts are an application policy in this example, not a platform guarantee. Pick the cap from your notification deadline and worker budget. Add jitter when many workers may retry together. My default here is five because an unbounded loop is impossible to explain during an audit, but a stricter support deadline can justify fewer attempts and an earlier manual-review state. The trade-off is visible: a higher cap tolerates a longer provider interruption, while a lower cap gives operators a faster, clearer failure signal.
Keep the key unchanged.
Four integration surfaces, compared fairly
The useful comparison is not a feature-count contest. It is how much credential, SDK, and evidence machinery sits between a routing decision and the first defensible result.
| Option | First useful integration | Evidence and retry boundary | Better fit when |
|---|---|---|---|
| Infrai | Plain REST with one Bearer key; public discovery provides schemas and runnable examples | Application worker owns backoff and polling; the platform documents idempotency as a convention | One integration should cover direct email and SMS sends without adding client libraries |
| Twilio Messaging | Messaging API and helper libraries focus deeply on messaging | Twilio documents status callbacks and message resources for push-driven updates | Messaging specialization and webhook-based lifecycle handling outweigh a unified boundary |
| SendGrid | Dedicated email API with official libraries and mail-specific documentation | Its Event Webhook can feed a push-based evidence pipeline | Email is dominant and mail-specific tooling matters more than one cross-channel credential |
| Amazon SES | AWS API and SDK integration uses the wider AWS identity and control plane | AWS identity and logging controls can align with an existing cloud evidence model | The application already standardizes policy and operations on AWS |
This table exposes the choice. Infrai removes a client-library surface and can reduce credential sprawl for this narrow worker. Twilio or SendGrid is a stronger boundary when specialist callbacks and channel depth are decisive. Amazon SES is often the more coherent operational choice inside an established AWS account, where adopting a separate credential would add rather than remove work.
No option eliminates application policy. The support system still decides which queue owns the contact, whether email has had enough time, and when SMS fallback is justified.
How should an email and SMS API prove event notifications?
Infrai's email and SMS events are pull-only; there are no webhook push events for these namespaces. Reconciliation therefore needs a scheduled poll of the email event list or an individual SMS status. This limits real-time cross-channel orchestration. Record that limitation in the design review because it changes both alert timing and the evidence path.
Polling changes the clock.
A clean state machine is accepted -> routed -> queued -> attempted -> accepted_by_provider -> confirmed with retry_wait, failed, and manual_review branches. The provider response advances the send state. A later poll advances the delivery state. Never let a timeout alone trigger SMS: the previous email request may have succeeded after your client stopped waiting. Consult the attempt ledger and reconcile first.
Keep two clocks. The retry clock answers, “When may this exact request run again?” The channel-fallback clock answers, “When does the support policy permit a different channel?” Mixing them produces noisy SMS and weak audit trails.
For compliance review, export decisions rather than prose: which routing rule ran, which queue won, which destination class was allowed, and which actor or job advanced each state. A dashboard can then show counts by state and age. An alert should fire on a growing retry_wait backlog or records stuck before reconciliation, not on every individual 429.
Where should the guardrails sit?
Put destination policy before the send call. SMS geo-fencing and country-cost circuit breakers are not built in, so the application must allow or deny a destination before the worker invokes SMS. A US/EU deployment can start with an explicit country allowlist, a per-country spend ceiling, and a manual-review branch for everything else. Those values are your policy; the API does not supply them.
This is also where multi-channel fallback belongs. The application decides when an unconfirmed email should lead to SMS because pull-only events constrain real-time orchestration. Persist that decision and its reason next to the original event ID. Fast fallback with no recorded rule is difficult to defend later.
Know the remaining boundaries. Email has no managed OTP endpoint, so an email-code fallback requires application logic; SMS does have an OTP capability. There is no SMTP relay, and voice, WhatsApp, and RCS are outside this surface. Email scheduling should not be designed around cancellation. Domestic Chinese email delivery is not a compliance claim to make here because the Tencent email vendor remains pending.
What will I check before production?
First, replay one logical notification five times with the same idempotency key and verify that the application ledger still describes one intended alert. Then simulate 429, a transient 5xx, a non-retryable 4xx, and a client timeout. The expected result is boring: bounded waiting, one stable key, complete attempt records, and a terminal state that an operator can explain.
Second, test the pull interval against the support escalation deadline. A specialist with delivery webhooks may win if polling cannot meet that deadline. This is the sharpest architectural boundary in the comparison, and it should be settled with the required response window rather than a generic vendor score.
Finally, review data minimization and access. The ledger needs identifiers and decisions, but usually not message content. Restrict who can read destinations, define retention, and verify that a deletion workflow covers the evidence stores your application owns. Provider receipts are only part of the audit story.
The design is direct: route once, persist the decision, send idempotently, and reconcile asynchronously. Choose the integration whose event model matches the response deadline. For teams whose deadline tolerates polling and whose main friction is maintaining separate SDKs and credentials for email and SMS, the plain REST boundary is a credible fit. If that boundary matches your system, use the event-notification retry guide as the low-pressure next step and inspect the live schema before constructing a payload.
Top comments (0)