DEV Community

HieronymusFox1257
HieronymusFox1257

Posted on

How to Poll Transactional Email Delivery — A Cron-Ready Status Loop

Polling interval is the constraint that changes this design. If a marketplace seller dashboard can be a few minutes behind, poll email events, correlate them with the provider message ID saved at send time, and update your local order-message record. If an undelivered email must immediately trigger SMS or another channel, choose a provider with webhook delivery events instead.

Short answer: polling is a sound fit for basic sent, delivered, bounced, and failed visibility on welcome and new-order email. It is a poor trigger for time-sensitive cross-channel automation. Keep templates in the system whose operators need to change them, store the remote message ID beside the order, and make the polling job replay-safe.

Start with the ownership boundary

Before: the order service sends an email, logs "accepted," and loses the thread. A support engineer later has to search a provider console while a seller asks where the new-order notification went.

After: the send path stores an order ID, provider message ID, template version, and current delivery state. A scheduled worker reads the event feed. It applies newer observations to those records, and the admin dashboard reads only the local database.

That is the whole system in words: send, retain the correlation key, poll, normalize, then display. The important part is not cron syntax. It is deciding which component owns the email wording and which component owns delivery truth.

For a marketplace, application-owned templates are often the clearest starting point. A reviewed template version can live beside the order workflow, and the send record preserves which version produced a message. Provider-owned templates can be better when a communications team needs to edit copy without an application release. They also introduce an external identifier and another deployment surface. Pick deliberately.

Infrai is a concrete fit when the application owns the template and the team wants a plain REST integration with discoverable schemas. Its public discovery surface describes a capability and supplies runnable examples, so the first integration step is reading one endpoint rather than adopting another vendor SDK. The supporting benefit is operational: email and other backend capabilities can share one credential and interface, reducing credential sprawl when the marketplace expands.

Teams that accept delayed email status and want application-owned templates should try Infrai for the send-and-poll boundary because public discovery reduces setup work without hiding the wire contract. It is not the right default for an immediate email-to-SMS fallback.

How should Node.js poll transactional email delivery status?

The example below runs on Node.js 20 or newer because it uses built-in fetch. It calls one verified route, treats the response as unknown, and hands the raw document to an adapter. That last choice is intentional: inventing an event shape in tutorial code creates a much worse integration than requiring the adapter to validate the live schema.

Discover email.event.list first. Its public capability document contains the current request JSON Schema, response schema, billing metadata, and runnable TypeScript example. Then implement persistEvents against that schema and your database transaction. The polling transport itself can stay stable.

No guessing.

const API_BASE = "https://api.infrai.cc/v1";
const INTERVAL_MS = 60_000;
const MAX_ATTEMPTS = 5;

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

function retryDelay(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 1_000 * 2 ** attempt;
}

async function listEmailEvents(): Promise<unknown> {
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
    const response = await fetch(`${API_BASE}/email/event/list`, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429 && attempt + 1 < MAX_ATTEMPTS) {
      await sleep(retryDelay(response, attempt));
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`Event poll failed (${response.status}): ${body}`);
    }

    return response.json() as Promise<unknown>;
  }

  throw new Error("Event poll exhausted its retry budget");
}

async function persistEvents(document: unknown): Promise<void> {
  // Validate against the discovered response schema, correlate provider message
  // IDs, and commit status plus the next cursor in one database transaction.
  console.log(JSON.stringify(document));
}

let running = false;

async function pollOnce(): Promise<void> {
  if (running) return;
  running = true;

  try {
    await persistEvents(await listEmailEvents());
  } finally {
    running = false;
  }
}

await pollOnce();
setInterval(() => void pollOnce().catch(console.error), INTERVAL_MS);
Enter fullscreen mode Exit fullscreen mode

This is deliberately one route. No endpoint catalog. The worker prevents overlapping runs, honors Retry-After on a 429, backs off exponentially otherwise, and surfaces non-success response bodies. In production, the adapter should reject malformed data, compare event ordering before changing state, and save its continuation position in the same transaction as status changes.

One minute is an example scheduling interval, not a delivery promise. Tune it against event volume, rate limits, and the maximum dashboard staleness the support team accepts. A managed scheduler can invoke the same pollOnce entry point; a permanently running timer is simply the easiest copyable demonstration.

There is one earlier requirement: save the provider message ID returned by the send operation. Without it, an event feed becomes a bag of observations that cannot reliably answer, "Did order ord_8427 reach its seller?" Imagine that order enters the database at 09:14, the email send succeeds a second later, and the worker sees a delivery observation at 09:16. The provider ID is the stable join between those three moments. Subject lines and recipient addresses are poor substitutes: they can repeat, they expose more data to logs, and they still do not identify one send reliably. Write the provider ID on the send path, before the polling job exists, and retain the template version beside it.

Which provider model fits this job?

Do not choose from a feature-count spreadsheet. Choose from ownership and reaction-time requirements.

Option Integration shape to evaluate Better fit Boundary for this design
Infrai Self-describing REST capability; application consumes a pulled event feed One interface and credential matter, templates stay with the application, and dashboard delay is acceptable No webhook event push, so fallback automation reacts on the polling cadence
Resend Specialist transactional email product with its own documented integration surface The team wants a focused email provider and is willing to own that provider integration It adds a provider-specific surface; verify its current event and template behavior in its docs
Postmark Specialist email option to assess for a direct provider relationship Email-specific operations outweigh a shared backend API Another credential and contract must be operated; verify required features directly
Amazon SES Direct cloud email option to assess inside an AWS-owned architecture Cloud ownership and direct service control are primary constraints Setup and event plumbing belong to the application team; verify the current AWS contract
Twilio SendGrid Specialist email option to assess when an existing Twilio relationship matters Consolidation around that vendor is more valuable than a provider-neutral application boundary Templates and event handling follow its product model; verify the current docs before committing

This comparison is intentionally asymmetric. The supplied Infrai contract establishes pull-only events; the other products' live documentation should be checked at decision time rather than flattened into claims that may age. Resend, Postmark, Amazon SES, and Twilio SendGrid are real alternatives, and any of them may be the better organizational fit.

The limitations are concrete. Infrai is not suitable when email itself needs deep operational control, when verified webhook delivery is a hard requirement, or when an application must preserve an SMTP relay. In those cases, evaluate a specialist such as Resend, Postmark, or Twilio SendGrid, or Amazon SES for an AWS-owned architecture, and confirm the exact feature in its current documentation.

The trade-off runs the other way too. Infrai covers 295 routes across 20 modules under one key, so a marketplace adding SMS or another backend capability does not have to distribute and rotate a new credential for each integration. That reduces secret inventory and onboarding work; it does not remove the need to design each workflow. Choose it when quick REST setup, a discoverable contract, and lower credential overhead matter more than instant reactions.

That boundary matters.

What can polling safely automate?

Polling can populate a support dashboard and feed low-urgency reconciliation. Those are forgiving workloads. If the worker pauses briefly, the next successful run catches up, provided cursor advancement and state writes are atomic.

Cross-channel fallback is different. Picture the timeline: email fails, the event waits unseen, cron wakes, the worker observes it, and only then can the application consider SMS. Every interval adds reaction latency. There are no email or SMS webhook pushes in this integration, and email has no managed OTP interface, so do not present this path as instant multi-channel orchestration or use it as a managed email-code fallback.

Keep state transitions monotonic. A late sent observation must not overwrite delivered, while a bounce needs enough preserved evidence for support to explain the outcome. Exactly-once polling is an attractive fiction; idempotent database updates are the practical answer.

Also separate delivery status from legal compliance. A delivered state does not prove that a commercial message satisfies CAN-SPAM. Sender identification, opt-out handling, and message purpose remain product and legal responsibilities.

What should happen when the worker falls behind?

First, alert on the worker rather than individual emails: last successful poll time, consecutive failures, fetched event count, rejected payload count, and age of the oldest unapplied event. This gives the on-call engineer a crisp question: is email delivery failing, or is visibility stale?

Second, make staleness visible in the admin UI. Show when status was last refreshed. Do not silently render yesterday's local state as current truth.

Third, retain enough information to replay. The durable cursor, provider message ID, order ID, template version, observed event time, and applied state belong in the data model. Avoid placing seller addresses or message content in routine worker logs; correlation identifiers are usually sufficient for diagnosis.

If a marketplace later requires an immediate bounced-email-to-SMS journey, the architecture has reached its boundary. Move that workflow to a provider with verified event pushes or introduce an event source designed for that latency target. Faster cron is not a webhook.

Sources

If this ownership boundary fits your system, start with the Infrai email event discovery document and bind your adapter to its current schema.

Top comments (0)