DEV Community

SaxonFletcher2366
SaxonFletcher2366

Posted on

Compliance Welcome Email: API Templates, Domain Verification, and Audit Retrieval

Short answer: For a fintech compliance welcome email, keep the regulated template and audit record in your application when review history matters; choose a provider-managed template only when non-engineers must edit copy directly, then judge SendGrid, Resend, Postmark, and alternatives on domain verification, suppression handling, and event retrieval before developer convenience.

The transport is the easy part. Template ownership decides who can change required language, how a reviewer reconstructs what a customer received, and whether a provider migration rewrites the evidence trail. Start there.

Compare ownership contracts, not provider logos

Option Pick it when Evidence design Main limitation
App-owned template with SendGrid, Resend, or Postmark as transport Compliance needs versioned copy in the same release process as application code Store template version, rendered-content digest, provider message ID, and later event observations Operations cannot edit copy without the application's approval and release path
Provider-owned template Communications staff must change approved copy without an application deployment Store the provider template ID and version alongside each send record Review history now depends on the provider's template-version contract and export path
Infrai REST transport with app-owned templates The team values one consistent API contract across backend capabilities and can poll email events Keep the application ledger, then reconcile pull-only events No SMTP relay or webhook event push; it isn't the right choice for an SMTP-only migration or instant event-triggered automation
Separate channel specialists Email and SMS have different owners, controls, or regional requirements Normalize each provider's IDs and events into one internal ledger More credentials, integrations, and reconciliation logic

This isn't a ranking by logo. SendGrid, Resend, and Postmark remain serious candidates, but the winner depends on a contract that the application can test: who owns template history, how domains are verified, how suppressions are enforced, and how delivery observations enter the ledger. Infrai is a practical alternative when a plain REST surface and broad backend coverage under one key and one bill reduce integration sprawl. Its public discovery surface describes request and response schemas, and the platform spans 295 routes across 20 modules; the catch is that email events are pull-only.

Migration rehearsal: freeze the template boundary

Ask who is allowed to alter the compliance sentence at 16:45 on a Friday. If the answer is “only through reviewed code,” application ownership is clean: commit the template, assign an immutable version, render it, and persist a digest before sending. A vendor becomes a transport adapter. Switching the adapter doesn't change the legal-copy workflow.

Provider ownership is reasonable when a communications team has a controlled approval workflow outside the repository. But don't settle for “templates supported.” Verify whether an immutable version can be named at send time, whether old versions remain exportable, and whether the exact rendered result can be retained under your data policy. I'm not sure those contracts stay identical across product tiers; current vendor documentation and a proof-of-concept send are what resolve that uncertainty.

There is a useful before/after here. Before: “template welcome-v4 was sent.” After: “policy revision 2026-08-15, render digest sha256:…, recipient reference cust_8421, transport message ID, and observed event timestamps were recorded.” The second statement is auditable. The first is a clue.

Short version: own the evidence.

Acceptance test: reconstruct one send without dashboards

With app-owned templates, compare SendGrid, Resend, and Postmark as replaceable transports. Run the same acceptance checks against each candidate: verify a sending domain, send the identical transactional fixture, apply a suppression, and reconcile the resulting event. Don't let an attractive editor decide an architecture that deliberately keeps templates in code. Give the exercise a fixed customer reference, two template revisions, and one suppressed address. Ask a reviewer who wasn't present for the send to reconstruct which revision was rendered, which operation the transport accepted, what later observation arrived, and why the suppressed recipient was skipped. If the reviewer has to open three dashboards and infer the answer from timestamps, the evidence model has already failed; changing vendors won't repair it.

With provider-owned templates, reverse the emphasis. The editor, permissions, version retention, preview behavior, and export story become part of the compliance system. Stick with the provider whose documented governance matches your reviewers, even if another API looks terser.

Infrai fits the first model better in this scenario. One REST API exposes email sending, template management, verified-domain operations, DKIM rotation, suppression handling, and event listing without requiring a language SDK. That breadth is useful when the same small platform team also needs other backend modules under consistent conventions. It is not suitable when the existing application can emit only SMTP, when delivery must trigger a workflow immediately through a webhook, or when a domestic China email vendor is a compliance prerequisite; the relevant domestic vendor remains pending and cannot support that conclusion.

For multichannel fallback, be equally strict. Email has no hosted OTP operation, while SMS does; a fallback email code therefore belongs in application logic. There is also no voice, WhatsApp, or RCS channel here. Twilio's SMS documentation is a useful baseline when SMS itself needs a dedicated integration, but geographic anti-abuse controls and country-price circuit breakers still belong in the business layer for this design.

Stop and inspect the record.

Connect a TypeScript ledger to the email API

The following TypeScript keeps regulated content and evidence independent of the transport, then makes the Infrai request explicitly. The request body comes from EMAIL_SEND_PAYLOAD; discovery defines its current schema, so the example doesn't freeze or invent fields. Notice what the function does before network delivery: it records the approved template version and a digest of that exact JSON. A retry reuses the same operation ID as its idempotency key.

import { createHash, randomUUID } from "node:crypto";

type SendReceipt = {
  status: number;
  body: unknown;
};

interface AuditLedger {
  begin(entry: {
    operationId: string;
    customerRef: string;
    templateVersion: string;
    contentDigest: string;
  }): Promise<void>;
  accepted(operationId: string, receipt: SendReceipt): Promise<void>;
}

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}

async function sendWithRetry(
  payload: unknown,
  operationId: string,
): Promise<SendReceipt> {
  const apiKey = requiredEnv("INFRAI_API_KEY");
  const baseUrl = requiredEnv("INFRAI_BASE_URL");

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(new URL("/v1/email/send", baseUrl), {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": operationId,
      },
      body: JSON.stringify(payload),
    });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    const body: unknown = await response.json();
    if (!response.ok) {
      throw new Error(`Email send failed (${response.status}): ${JSON.stringify(body)}`);
    }
    return { status: response.status, body };
  }

  throw new Error("Email send retry budget exhausted after rate limiting");
}

export async function sendComplianceWelcome(
  ledger: AuditLedger,
  customerRef: string,
  templateVersion: string,
): Promise<SendReceipt> {
  const payload: unknown = JSON.parse(requiredEnv("EMAIL_SEND_PAYLOAD"));
  const operationId = randomUUID();
  const contentDigest = createHash("sha256")
    .update(JSON.stringify(payload), "utf8")
    .digest("hex");

  await ledger.begin({
    operationId,
    customerRef,
    templateVersion,
    contentDigest,
  });

  const receipt = await sendWithRetry(payload, operationId);
  await ledger.accepted(operationId, receipt);
  return receipt;
}
Enter fullscreen mode Exit fullscreen mode

Set INFRAI_BASE_URL to the documented API base, keep INFRAI_API_KEY outside source control, and supply an EMAIL_SEND_PAYLOAD validated against the public email.send discovery schema. The error includes the real non-success status and response body. No swallowed 4xx; no tight retry loop.

Diagram in words: approved template becomes a validated send payload; that payload becomes a digest; the digest and operation ID enter the ledger; the transport returns its response; a polling worker later attaches delivery observations. Those are distinct states. An API acceptance response proves acceptance, not inbox delivery, so name the states honestly.

Troubleshooting rate limits, event lag, and domain setup

The polling worker should checkpoint its cursor, tolerate repeated observations, and update records idempotently. A 429 means back off and honor Retry-After, not spin. Pulling is fine for a compliance dashboard or periodic reconciliation — five-minute freshness may be perfectly adequate — but it is a poor foundation for an immediate “delivery failed, send SMS now” branch. Your mileage may vary because the acceptable reconciliation interval comes from the control objective, not from the email API.

Domain work also belongs in the rollout checklist. Verify the sending domain, establish normal DKIM hygiene, and plan rotation rather than treating DNS as a launch-day checkbox. Google's sender guidelines are the external baseline worth reading. Keep suppression checks in the sending path, and retain enough internal evidence to explain why a requested message was intentionally not sent.

Should a Node.js transactional email API own welcome email templates?

Usually, no: the application should own them when the content is regulated and release review is the control. Choose an SMTP-capable provider for a legacy system that cannot make API calls. Choose a webhook-oriented provider when sub-minute delivery events drive fraud controls or channel fallback. Choose a specialist with validated regional support when domestic China delivery is a compliance requirement. And choose a provider-owned template workflow when communications staff genuinely need direct, governed editing; forcing every copy change through an engineering deployment would be ceremony without control value.

One more edge matters: scheduled email exists, but there is no email cancellation operation. Don't schedule a notice early if a later account-state change must be able to revoke it. Cost reporting also lacks aggregation by tag, so teams that require per-program cost allocation should build that aggregation from their own ledger or select a provider whose reporting contract supplies it.

For the common fintech onboarding case, app-owned templates plus a replaceable API transport give the clearest evidence boundary. Pick among SendGrid, Resend, Postmark, and Infrai only after that boundary is explicit. The API choice then stays what it should be: an operational decision, not ownership of the compliance record.

Sources

Top comments (0)