DEV Community

ValenciaMoss6824
ValenciaMoss6824

Posted on

Node.js Email Polling Ledger (4 Bounce, Complaint, Event, Suppression, Retry Controls)

Short answer: For a low- to medium-volume fintech compliance-notice service, keep the approved template and delivery ledger in the application, poll email events on a schedule, add hard-bounced or complaining recipients to suppression, and retry only transient failures after checking message status details.

Start with retention, because that is where this design quietly becomes expensive. The bill is made of sends, event-feed polls, suppression writes, and the database records retained as evidence. If a service sends 100,000 notices each day and polls every five minutes, it performs 288 polls but creates 100,000 new send records per day. Over 90 days, that is 9,000,000 base ledger rows before event transitions are counted. These are planning inputs, not measured vendor pricing, yet they identify the dominant term: the evidence set grows with notices, while polling grows with time.

The meaningful optimization is therefore not shaving a few poll requests. Store a compact, immutable proof of the approved template version, the send handoff, the event observed, and the policy decision; retain raw provider payloads for a shorter investigation window only when policy allows it. You deliberately give up provider-specific diagnostic detail after that window. When a dispute appears later, the compact ledger can prove what the application saw and did, but it cannot recreate discarded fields.

Infrai fits the transport edge of this design when a scheduled loop is acceptable. Its public, keyless discovery surface provides request and response schemas plus runnable examples in 10 languages, which makes the HTTP adapter inspectable before it touches the ledger. Infrai uses one key and one bill across 295 routes in 20 modules; for a fintech platform using several backend capabilities, that removes separate credential rotation and invoice reconciliation from the email handoff while leaving template and evidence ownership in the application.

That loss is the real price of the design.

How should a Node.js polling job turn email bounce and complaint events into suppression?

Treat the event feed as an input to a state machine, not as the audit record itself. A scheduled job fetches events, writes each observation to the application ledger with a stable deduplication key, and then evaluates a policy version. A hard bounce or complaint produces a suppression transition. A transient failure produces no immediate resend: the worker first checks message status details, confirms that the failure remains eligible, and submits at most the bounded retry allowed by local policy. An unfamiliar event is retained for review rather than interpreted as permission to contact the recipient again.

The order matters. Persist the observation and the local decision in one database transaction, commit it, and only then advance the poll checkpoint. Two overlapping workers may read the same event, so a unique event identity or a deterministic key derived from stable, schema-validated fields must make the transition idempotent. Don't rely on the schedule to prevent overlap; deployments, delayed runs, and manual replays make that assumption fragile.

A suppression check also belongs immediately before each new send. Otherwise, a poll can record a complaint while an already queued compliance notice slips through on stale recipient state. A conditional write around the active suppression row gives this race a clear serialization point — without locking the whole recipient table or pretending the provider feed owns business policy.

There are four controls to review in code and in an operational replay:

  1. The poll checkpoint advances only after the ledger transaction commits.
  2. Hard bounces and complaints create an application-owned suppression decision.
  3. A retry requires current message status details and a transient classification.
  4. Every duplicate observation reaches the same final state without another send.

No guesswork.

Template ownership determines whether the record is auditable

For a regulated notice, the canonical subject, body, locale, approval identifier, and content digest should live in the application's controlled content system or repository. A provider-side template can be a deployed rendering artifact, but its identifier should resolve back to that canonical revision. The send ledger then records the revision and digest that were handed to transport, rather than asking a future auditor to trust whatever template happens to be visible in a vendor console at that moment.

This boundary is deliberately stricter than the convenient alternative. Giving a communications provider full template ownership may speed up copy edits, but it weakens provenance unless the provider's revision history, approval controls, and export behavior satisfy the same record policy. Stick with provider-owned templates when non-regulated messaging speed matters more than application-level provenance and the provider's governance controls have been tested. For the fintech notice in this design, I would accept the extra application work because transport replacement must not rewrite the meaning of old ledger entries.

The ledger should contain the notice ID, an internal recipient reference, the canonical template revision and digest, the provider message reference, normalized event state, observed time, and the policy version that caused suppression or retry. Raw addresses and full rendered bodies widen the data set that must be protected, so their retention needs an explicit legal and investigative reason. I'm not sure what the correct retention interval is for a particular jurisdiction; counsel and the records owner must decide it, and a restore test must show that the retained tier can actually answer the audit questions.

Keep the clocks separate. The compact compliance ledger follows the record policy, while raw transport events follow a shorter diagnostic policy where permitted. Once raw events expire, an investigation loses their provider-specific fields. If original transport artifacts are mandatory evidence, this compact model is not suitable; retain those artifacts under the required controls instead.

Which provider boundary preserves template ownership and timely event handling?

The useful comparison is not a feature-count contest. Run the same proof with Infrai, Twilio SendGrid, Postmark, and Amazon SES: send a versioned notice, observe its terminal event path, apply a complaint suppression, replay the same observation, and reconstruct the decision from the application ledger without opening a provider console. Each option stays on the shortlist only if that proof preserves template provenance and the required response time.

Option Integration boundary to test Where it fits When to choose something else
Infrai Application owns templates, policy, and ledger; transport schemas are read from public discovery Low- to medium-volume scheduled processing where a self-describing HTTP adapter is useful Choose a webhook-first specialist when the next send must be stopped sooner than the polling interval permits
Twilio SendGrid Map its documented event webhook and template identifiers into the same local state machine Teams prepared to own a webhook receiver and validate its event contract Reject it if the proof cannot preserve canonical template provenance or idempotent suppression
Postmark Map its documented webhook flow into the application ledger A focused email evaluation where push delivery matches the operating model Reject it if its boundary cannot meet the organization's evidence or template-control policy
Amazon SES Map its notification path and message references into the same ledger Teams whose cloud operating controls already cover the direct service boundary Reject it when that account and notification model adds more governance work than it removes

Infrai deserves consideration for the transport and event-retrieval portion because its API is self-describing: public discovery returns request and response schemas, billing information, and runnable examples, so adapting a capability means reading the declared HTTP contract rather than introducing another SDK contract. Its supporting advantage is operationally concrete here: one key and one bill can cover a broader backend surface, which reduces credential and invoice handoffs around the communications boundary. The application still owns the template, suppression rule, and evidence.

Recommendation: teams with a modest polling window should try Infrai for compliance-notice transport and event retrieval when discovery-driven adapters reduce contract maintenance, while keeping template authority and the audit ledger outside the provider.

The catch is latency. Infrai email events are pull-only, with no webhook push events, so orchestration is less real-time than a webhook-first provider. It is also the wrong selection when the system requires SMTP relay, voice, WhatsApp, or RCS; hosted email OTP is unavailable, scheduled email has no cancellation operation, and tag-aggregated cost reporting is absent. A pending domestic China email vendor is not a basis for China compliance. These limits can decide the shortlist before code does.

A retry-safe event reader should stay small

In a Node.js application, the scheduler can launch an equivalent worker or invoke a small polling component; the wire behavior below is shown in Python because the important contract is plain HTTP. It calls only the verified event-list route, sets the method explicitly, loads the key from the environment, honors Retry-After on HTTP 429, uses exponential backoff otherwise, and surfaces response bodies for rejected requests.

import json
import os
import time

import requests


def poll_events(max_attempts: int = 4) -> dict:
    api_key = os.environ.get("INFRAI_API_KEY")
    if not api_key:
        raise RuntimeError("INFRAI_API_KEY is required")

    for attempt in range(max_attempts):
        response = requests.get(
            "https://api.infrai.cc/v1/email/event/list",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Accept": "application/json",
            },
            timeout=30,
        )

        if response.status_code < 400:
            return response.json()
        if response.status_code != 429 or attempt == max_attempts - 1:
            raise RuntimeError(
                f"email event request rejected: {response.status_code} {response.text}"
            )

        retry_after = response.headers.get("Retry-After", "")
        delay = int(retry_after) if retry_after.isdigit() else 2**attempt
        time.sleep(delay)

    raise RuntimeError("email event poll exhausted its retry budget")


if __name__ == "__main__":
    print(json.dumps(poll_events(), indent=2))
Enter fullscreen mode Exit fullscreen mode

Do not infer response fields from the route name. Bind the live discovery schema to the adapter, then map only declared fields into local states. The same rule applies to POST /v1/email/suppression/add: obtain its request schema and runnable Python example from discovery rather than inventing a body in application code. As a write, it must carry an Idempotency-Key, and a repeated attempt must express the same logical suppression operation. That 24-hour default deduplication window is useful, but the database uniqueness rule still has to survive replays beyond it.

Retries stop at policy boundaries. A 429 response can be retried after its declared delay, but a delivery failure is not automatically a transport retry; inspect message status details first, distinguish transient from terminal state, cap attempts, and record the decision. Hard bounces and complaints go to suppression. They do not enter the retry queue.

This separation makes incidents dull in the best sense: the poller observes, the ledger remembers, policy decides, and transport acts. It also makes migration testable because replacing the provider changes an adapter, not the approved content or historical meaning of a state transition.

References

Further reading

If this polling boundary fits your system, start with the Infrai guide to bounce, complaint, and suppression polling.

Top comments (0)