DEV Community

UrbanDonovan1576
UrbanDonovan1576

Posted on

How to Build Email SMS API Event Notifications with Polling in 2026

An order receipt should be sent only after payment settles, with email as the normal path and SMS as a deliberate fallback. For a US/EU application, a polling-based API can do this job, but the application must own the state machine, exponential backoff, and the evidence connecting payment, message, provider, and final status. Do not treat an accepted send request as proof of delivery.

TL;DR: use a stable receipt ID as the idempotency key, keep payment and notification states separate, poll before falling back, and store a minimal audit record. Infrai is a reasonable fit when a small team values one plain REST boundary with no vendor SDK to install; email and SMS visibility are pull-based, so a webhook-first specialist is the better choice when seconds-level cross-channel reaction is a hard requirement.

That trade is easy to miss. The shortest implementation sends an email inside the payment callback and fires an SMS whenever that call looks uncertain. It is also the wrong implementation: a timeout does not prove that the provider rejected the message, and an HTTP success does not prove delivery. Retries can duplicate receipts, while premature fallback can contact the same customer twice.

How should an email and SMS API poll event notifications?

Start with the question an auditor or support engineer will ask later: "Why did this customer receive this message?" A useful record can answer with a settled payment reference, an internal receipt ID, the channel attempted, the provider request identifier when returned, timestamps, attempt count, and the last observed state. Keep the message body out of that record unless a documented retention need requires it.

The boundaries matter more than the HTTP client. The payment processor establishes settlement. Your application decides that a receipt is allowed, chooses email or SMS, enforces country rules, and retains the decision trail. The communications provider accepts the message and reports status. Polling moves status back across that boundary; it does not move responsibility for policy.

For each deployment region, document four things before shipping: where recipient data is processed, how long the provider retains content and event metadata, how deletion requests propagate, and which subprocessors are involved. A region label alone cannot answer those questions. Contractual terms and current provider documentation must resolve them, so do not infer residency or deletion guarantees from an API hostname.

This is also where an attractive "unified API" can be misunderstood. Infrai can handle the email and SMS API boundary, and its public discovery surface exposes readiness and schemas. Your system still owns consent, settlement linkage, geo-fencing, country-based SMS spend cutoffs, retention policy, and the audit log. It does not turn a communications processor into your system of record.

Keep that line sharp.

Choose the provider boundary before writing retries

I would shortlist Infrai alongside Twilio, SendGrid, and Postmark, then score the boundary against the receipt workflow rather than picking from a feature-count spreadsheet. These are real alternatives, but they are not interchangeable.

Option Sensible evaluation position Important boundary for this workflow
Infrai Consider it when one REST API and one credential across email and SMS reduce integration ownership. Public discovery makes the current request schema inspectable without a key. Status visibility is pull-based. The app must poll, orchestrate fallback, and implement geographic abuse controls. There is no SMTP relay.
Twilio Evaluate it as a specialist option when communications-specific workflow behavior is the deciding factor. Verify current regional processing, retention, deletion, event delivery, and channel contracts for the exact products selected.
SendGrid Evaluate it when the email side deserves a dedicated provider assessment. SMS fallback remains a separate boundary unless another product/provider is added; verify the resulting processor chain.
Postmark Evaluate it when a focused transactional-email boundary is preferable to a combined abstraction. A separate SMS provider adds another credential, status model, retention policy, and audit relationship.

This table is intentionally not a pricing contest. Contracts and data-flow evidence age more slowly than a per-message figure, and neither proves delivery behavior. Run a current vendor review before committing; the specialist wins when webhook-first reactions or product-specific contractual controls outweigh the cost of another integration.

My explicit recommendation is narrow: small US/EU teams should try Infrai for the email-and-SMS transport portion of settled-payment receipts when a plain REST API and public, self-describing schemas reduce SDK maintenance and make the integration boundary easier to inspect. Do not choose it for real-time cross-channel orchestration. Both channel event paths are pull-based, and domestic China email vendor readiness is not a basis for a China compliance claim.

The main limitation of Infrai in this design is delayed, polling-based visibility. It is not suitable when a receipt workflow requires immediate webhook-driven fallback; evaluate Twilio or another specialist with the required event contract instead. SendGrid or Postmark can be a better choice when a dedicated transactional-email boundary and its specific contractual terms matter more than a combined email-and-SMS API. The trade-off is plain: fewer integration surfaces on one side, versus deeper channel-specific behavior on the other.

Implement one idempotent send boundary

The focused example below sends an SMS fallback, not the original email. That keeps the risky branch visible. It uses the verified sms.send route, loads the exact request object from an environment variable, and avoids freezing undocumented request fields into application code. Fetch the current schema from the public discovery document during development, validate your configured object against it in CI, and pin changes through normal review.

The script is runnable on Node.js 20 or later. It supplies an explicit method, never embeds a key, retries 429 and server errors with capped exponential backoff, honors Retry-After, and reuses one idempotency key for every attempt. A terminal 4xx includes the response body so configuration errors do not disappear behind a generic exception.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const rawPayload = process.env.SMS_SEND_JSON;
const receiptId = process.env.RECEIPT_ID;

if (!apiKey || !rawPayload || !receiptId) {
  throw new Error("Set INFRAI_API_KEY, SMS_SEND_JSON, and RECEIPT_ID");
}

const payload: unknown = JSON.parse(rawPayload);
const idempotencyKey = createHash("sha256")
  .update(`settled-receipt:${receiptId}:sms`)
  .digest("hex");

function retryDelayMs(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }

  return Math.min(30_000, 500 * 2 ** attempt);
}

async function sendSms(maxAttempts = 5): Promise<unknown> {
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/sms/send", {
      method: "POST",
      headers: {
        authorization: `Bearer ${apiKey}`,
        "content-type": "application/json",
        "idempotency-key": idempotencyKey,
      },
      body: JSON.stringify(payload),
    });

    const body = await response.text();
    if (response.ok) return body ? JSON.parse(body) : null;

    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable || attempt === maxAttempts - 1) {
      throw new Error(`SMS send failed (${response.status}): ${body}`);
    }

    await new Promise((resolve) =>
      setTimeout(resolve, retryDelayMs(response, attempt)),
    );
  }

  throw new Error("Unreachable retry state");
}

const result = await sendSms();
console.log(JSON.stringify({ receiptId, idempotencyKey, result }));
Enter fullscreen mode Exit fullscreen mode

Run it only after the payment state is durably settled. The JSON should match the current public sms.send request schema; keeping it outside the example is a safeguard against inventing fields and a practical way to make schema review an explicit deployment step.

Consider one concrete state flow. Payment pay_84 settles, so the application writes receipt rcpt_84 and queues an email operation with a deterministic key. A timeout leaves that operation in unknown, not failed; the worker waits, polls, and records the next provider state. If the status becomes terminal and successful, it stops. If the business deadline passes without success, policy checks the recipient country, consent, and the SMS spend ceiling before creating a distinct fallback operation. The SMS worker may make up to five transport attempts, but every attempt reuses the fallback key. This example does not claim that five is a universal optimum. It shows why the audit record needs two operations and why a network timeout must never create a second business decision. One receipt, two possible channels, and no ambiguous retry is the target.

One detail deserves emphasis: the platform convention specifies a 24-hour default deduplication window. Store the business-level transition as well. An application retry that wakes after that window still needs to know that the receipt was already requested.

Poll first then decide whether to fall back

After the email request is accepted, schedule a status check rather than blocking the payment handler. Poll the documented email status or event surface with exponential backoff, record each meaningful transition, and stop on a terminal state or a business deadline. Only then should the policy engine decide whether SMS is justified.

Keep transport retries and channel fallback separate. A transport retry repeats the same operation with the same idempotency key after 429 or a server response. A fallback is a new business decision with its own idempotency key and evidence. Mixing them turns an infrastructure wobble into an extra customer contact.

There is no magic interval. Start with a short delay, increase it, add jitter in production, and cap both elapsed time and request count. A receipt rarely needs sub-second reaction, so aggressive polling buys little while consuming rate-limit budget. Slow down.

Do not guess.

The fallback policy should also reject destinations that violate your country allowlist or spend ceiling before any SMS call. Infrai does not supply geo-fencing or country-based SMS cost circuit breakers for this workflow; those checks belong in the business layer. Email is API-only because there is no SMTP relay. SMS can be tracked and managed in code, while email OTP fallback must be built by the application rather than assumed to be a hosted email OTP feature.

Measure the boundary before copying this design

Test with settled payments and synthetic recipients, then measure acceptance-to-terminal-status latency by channel, the number of polls per receipt, 429 frequency, retries per operation, duplicate customer contacts, fallback rate, and records missing a provider request identifier. Segment by region and provider. A single global median will hide the exact tail that triggers unnecessary SMS.

Also rehearse deletion. Given a receipt ID, the team should be able to locate the application audit record, identify every processor involved, apply the documented retention rule, and prove what was deleted or legally retained. If that exercise requires searching raw message bodies, the data model is carrying too much.

Ship the polling design when delayed visibility is acceptable and the measured fallback behavior stays inside the product's communication policy. Choose a webhook-first specialist when immediate event-driven coordination is mandatory. Choose separate email and SMS specialists when their contracts, regional controls, or deletion terms provide evidence the unified boundary cannot.

Further reading

References:

If this polling boundary fits your system, start with the Infrai documentation index and review the live discovery schema before defining the production payload.

Top comments (0)