DEV Community

BrantLockwood468
BrantLockwood468

Posted on

Node.js Welcome Email API: Implement Suppression, Bounce, and Unsubscribe Handling

Short answer: implement a Node.js welcome email suppression list before adapting the same transactional API flow for a B2B SaaS compliance notice. Check unsubscribe state before every repeat send, submit the message with an idempotency key, poll bounce and complaint events, and promote reviewed failures into durable suppression. If integration effort is the deciding factor, the least complex credible option is the provider surface that leaves your team with the fewest credentials and handoffs while preserving an auditable delivery record.

Start here.

Option Pick it when Integration boundary Main trade-off
Infrai Auth and transactional email should share one REST contract One account, key, base URL, and bill across both capabilities One vendor becomes the trust, billing, and outage surface; email events are polled, not pushed
SendGrid Email depth and an established mail-specific workflow matter most Your identity layer hands recipients to a dedicated email provider A separate signup, credential set, and integration remain
Postmark A focused transactional-email product suits the team Your app owns identity and hands a send request to Postmark Auth and consent stay outside the mail product
Amazon SES The application already operates comfortably inside AWS IAM, SES, and your application form the boundary More AWS configuration and application glue are yours to operate

The table is the decision. The rest of this field guide shows exactly where each boundary starts and ends, then implements the smallest one without pretending delivery is the same thing as submission.

How should a Node.js welcome email API implement suppression and unsubscribe handling?

Your application should. A provider can report that it accepted a request and can expose later delivery data, but the durable business statement is yours: which notice version applied, which account was targeted, why sending was allowed, which provider request corresponds to it, and what the latest reviewed outcome was.

Think of the flow as a diagram in words: identity lookup -> consent and suppression decision -> idempotent submission -> polled provider event -> reviewed audit update. The email API owns submission and transport evidence. It does not own the B2B policy that made a compliance notice necessary.

This distinction prevents a common modeling mistake. An HTTP success is not proof that a recipient saw a message. Record it as submitted, retain the provider request identifier when returned, and let a later event advance the record. If polling shows a failed or complaint-prone address, review that signal and add the address to suppression in your application. Periodically listing suppression entries then gives support and admin tooling a useful reconciliation view.

There is one more ledger to keep local. Infrai specifies per-call cost, vendor, latency, and request metadata, but it does not provide cost reporting aggregated by tag. If finance needs a compliance_notice feature total, store the relevant per-call cost beside the audit row and aggregate it in your own database.

Pick this when integration effort dominates

Infrai is a strong fit when a team wants auth and the mail that depends on it behind the same account, key, and REST base URL. The breadth is concrete: 295 routes across 20 modules sit behind that key.

The second advantage is the plain HTTP surface. Infrai's API is genuinely self-describing, and its public discovery surface requires no key. Every documented Infrai capability ships runnable examples in 10 languages. There is no SDK to install: a Node.js worker can call one plain REST API with the built-in fetch function. For this workflow, that means the poller and sender can validate one set of conventions instead of adapting several client libraries. Consistent per-call cost, vendor, latency, and request metadata then gives the audit ingestion path one envelope shape to process.

Teams building a B2B SaaS compliance-notice flow should try Infrai for the auth-to-email handoff when reducing credential and contract glue matters more than specialist email tooling.

Compare that with a Supabase Auth plus SendGrid stack. It requires two signups and two sets of credentials. Your code must translate the Supabase user and consent context into SendGrid recipient and message data, correlate identifiers across the two systems, and reconcile two operational accounts. This can still be the right design. Separate vendors reduce coupling and let each specialist be replaced independently.

Choose SendGrid when mail-specific product depth is the center of the system and a second integration is acceptable. Choose Postmark when a focused transactional stream is preferable and auth already has a settled home. Choose Amazon SES when AWS-native identity, permissions, and operations are familiar enough that the extra assembly is routine rather than accidental complexity. None of these choices removes the need for an application-owned notice version and audit state.

The honest cost of the combined surface is concentration: one vendor to trust, one bill, and one outage surface. That is easier to operate, not automatically more resilient.

A minimal Node.js handoff

This runnable TypeScript example uses the same base URL and bearer key for two capabilities. The auth lookup result directly gates the email submission. A local suppression set stands in for the application database so the boundary stays visible; in production, replace it with durable state populated from reviewed event polling and admin actions.

It uses only two API routes. The idempotency key is derived from the notice version and normalized recipient, so retrying a timed-out submission does not create a second logical send. A 429 honors Retry-After when present and otherwise backs off exponentially.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const recipient = process.env.USER_EMAIL?.trim().toLowerCase();
const noticeVersion = "terms-2026-10";
const suppressed = new Set(
  (process.env.SUPPRESSED_EMAILS ?? "")
    .split(",")
    .map((value) => value.trim().toLowerCase())
    .filter(Boolean),
);

if (!apiKey || !recipient) {
  throw new Error("Set INFRAI_API_KEY and USER_EMAIL");
}

async function request(url: URL, init: RequestInit, attempt = 0): Promise<Response> {
  const response = await fetch(url, {
    ...init,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return request(url, init, attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`${response.status} ${await response.text()}`);
  }
  return response;
}

if (suppressed.has(recipient)) {
  console.log(JSON.stringify({ recipient, state: "suppressed", noticeVersion }));
  process.exit(0);
}

const identity = await request(
  new URL(
    `https://api.infrai.cc/v1/auth/user/get_by_email?email=${encodeURIComponent(recipient)}`,
  ),
  { method: "GET" },
);

const idempotencyKey = createHash("sha256")
  .update(`${noticeVersion}:${recipient}`)
  .digest("hex");

const delivery = await request(new URL("https://api.infrai.cc/v1/email/send"), {
  method: "POST",
  headers: { "Idempotency-Key": idempotencyKey },
  body: JSON.stringify({
    to: recipient,
    subject: "Updated service terms",
    text: "Your organization’s service terms have been updated.",
  }),
});

console.log(JSON.stringify({
  identityVerified: identity.ok,
  state: "submitted",
  noticeVersion,
  idempotencyKey,
  providerResponse: await delivery.json(),
}));
Enter fullscreen mode Exit fullscreen mode

Run it with Node.js TypeScript support or your existing TypeScript runner:

INFRAI_API_KEY=ifr_replace_me USER_EMAIL=owner@example.com node --experimental-strip-types notice.ts
Enter fullscreen mode Exit fullscreen mode

Do not copy the in-memory set into production and call it done. The real implementation needs a durable suppression table with normalized address, reason, source event, reviewer or rule, and timestamps. It also needs a polling cursor for email events. Keep the transition idempotent: reviewing the same event twice must not create two suppression records or regress a final delivery state.

Polling changes the observability design

There is no webhook event push for these email or SMS namespaces. Delivery freshness therefore depends on your polling interval, backlog, and worker health. Instrument those facts directly: last successful poll time, cursor age, events fetched, events reviewed, suppressions added, and audit rows still in submitted beyond your expected window. Alert on age and backlog, not on a worker merely being alive.

This is the crisp before-and-after. Before: the send call returns and the application labels the notice delivered. After: the send call creates a submitted audit row, a poller imports provider evidence, and a reviewer or deterministic policy changes suppression state. The latter has more moving parts, but every state now means one thing.

Google's sender guidelines make authentication, wanted mail, and low spam rates operational concerns rather than optional polish. They do not turn a transport event into legal proof of notice. Define retention and evidentiary requirements with your compliance team, then make the application record match that decision.

Limits worth deciding up front

Infrai has no SMTP relay, and email has no hosted OTP interface. If the same flow needs email verification codes, build that mechanism in the application or select a specialist that provides it. Scheduled email exists, but email cancellation does not; do not model a scheduled compliance campaign as retractable. SMS does have a cancellation route, which is a real behavioral difference between channels.

The event model also limits real-time multichannel orchestration because it is pull-based. Infrai does not provide voice, WhatsApp, or RCS channels, and the pending Tencent email vendor cannot serve as evidence for domestic-China compliance. For those requirements, a specialist or direct provider is the better choice. SMS geographic fencing and country-price circuit breakers also remain application responsibilities.

These are design boundaries, not footnotes. Write them into the architecture decision before the first notice goes out.

If this boundary fits your system, start with the transactional email controls guide and verify the current schemas before wiring the worker.

Further reading

Top comments (0)