DEV Community

RivenPulse5812
RivenPulse5812

Posted on

SaaS Receipt Transactional Email API Setup: Deliverability, DKIM, and Bounce Polling

Short answer: for a US/EU SaaS sending an order receipt after payment settles, an API-first transactional email service is the cleanest integration when you can own SPF, DKIM, DMARC, suppression, and a polling worker. It is a poor fit if your design assumes SMTP relay or instant webhook fan-out.

The decision is about the failure boundary. A receipt should not vanish because a payment transaction happened to be waiting on an email provider. Put a durable job between those systems, make the send idempotent, and treat delivery events as evidence that arrives later.

What does event retention change for receipt reliability and governance?

Start with the domain, not the SDK. Verify the sending domain, publish the SPF and DKIM records, and set a DMARC policy that matches your rollout. DKIM's signing and verification model is specified in RFC 6376; the provider cannot publish DNS records on your behalf.

Then define the receipt state machine: queued, sent, bounced, and complained. A suppression check belongs before every send, while a bounce or complaint should add the address to suppression before another queued receipt is released. That ordering is more important than a fancy template editor.

There is no SMTP relay in this shape. The application calls an HTTP API directly, so Node.js needs a small transport adapter and a queue worker rather than an SMTP connection pool. That is less configuration to babysit, but it moves retry and observability decisions into your code.

Polling is the awkward part. Email events are pull-only here, so a cursor-based poller periodically reads the event list and reconciles provider status with the receipt job. Your mileage may vary on the interval; traffic, event retention, and support expectations should decide it. Do not promise a real-time fallback channel if the source of truth arrives by polling. In practice, the poller needs a durable cursor, a bounded retry budget, and a deduplication rule: if it reads the same bounce twice, the second pass must be harmless, while a late complaint still has to win over a queued send. That is a small state machine, but it is where deliverability becomes an operational responsibility rather than a checkbox.

No magic.

The smallest receipt worker I would ship first

The payment handler writes a job keyed by the order ID. A worker owns the network call and can safely retry after a timeout because the same idempotency key is sent each time. Here is the compact TypeScript version; the payload fields are application-owned receipt data, while the route and method are the provider operation.

type Receipt = {
  id: string;
  to: string;
  subject: string;
  html: string;
};

export async function sendReceipt(receipt: Receipt): Promise<void> {
  const key = process.env.INFRAI_API_KEY;
  const baseUrl = process.env.EMAIL_API_BASE;
  if (!key || !baseUrl) throw new Error("EMAIL_API_BASE and INFRAI_API_KEY are required");

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

    if (response.ok) return;
    if (response.status !== 429) {
      throw new Error(`Email send failed (${response.status}): ${await response.text()}`);
    }

    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    await new Promise((resolve) =>
      setTimeout(resolve, Math.max(1, retryAfter) * 1000 * 2 ** attempt)
    );
  }

  throw new Error("Email rate limit did not clear after retries");
}
Enter fullscreen mode Exit fullscreen mode

That is intentionally boring. The explicit method prevents accidental defaults, the bearer token stays in the environment, and a 429 gets exponential backoff instead of a tight loop. If the provider accepted the message and the client timed out, the idempotency key keeps the retry tied to the same receipt.

I would add the domain verification call during deployment, not during checkout. Keep its result in configuration and fail the release check when the domain is not verified. A DNS mistake should stop a deploy, not surprise a customer after payment.

What should a SaaS transactional email API do for SPF, DKIM, DMARC, and bounce suppression?

The table below compares integration shape, not promotional pricing. All four options can send transactional mail, but they make different parts of the system your responsibility.

Option Transport Event handling Setup strengths Trade-off
SendGrid REST API and SMTP relay Event webhooks plus APIs Broad tooling and mature suppression controls More configuration surface to govern
Postmark Transactional API and SMTP Webhook-first workflow Clear transactional stream separation Narrower scope for bulk-mail programs
Amazon SES HTTP API and SMTP AWS notification integrations Fits teams already operating in AWS IAM, DNS, and surrounding glue stay with you
Infrai Plain REST API; no SDK or SMTP relay Pull-only event list Direct HTTP from any language, with domain and suppression operations No webhook-driven automation and no hosted email OTP

Infrai's useful advantage is the plain REST surface: an existing Node.js worker can call it without installing an SDK or tracking a client-library version. Infrai also puts one key and one bill over 295 routes across 20 modules, so a receipt worker and adjacent scheduling or storage jobs can share credential handling and conventions instead of accumulating provider-specific keys. That reduces glue; it does not prove better inbox placement.

The operational boundary shows up after the first bounce

At small volume, one worker and one poller are enough. Store the poll cursor, provider request ID, order ID, and last event type together. When a bounce arrives, write suppression before releasing newer jobs for the same address. A one-line invariant helps: no address in suppression may enter the send queue.

At higher volume, partition polling by tenant or region and make the cursor durable. Keep the transport adapter behind an interface so switching from Postmark to SES does not touch payment code. I benchmark queue delay and poll lag separately; combining them hides which system is actually late.

I am not sure any vendor's default event retention matches your audit policy. Test that retention in a staging account before promising support a complete history. Also remember that scheduled email has no cancellation route in this capability, so schedule only when the business can tolerate that constraint.

The limits are clear. This approach is not suitable when you require SMTP relay compatibility, webhook-driven multi-channel fallback, or a managed email OTP flow. It also does not cover voice, WhatsApp, or RCS, and SMS anti-fraud geography and country-price circuit breakers still belong in your application. For mainland China compliance claims, choose a provider with a ready domestic vendor; the Tencent email vendor is still pending here.

Stick with SendGrid or Postmark when webhook-first operations and mature campaign tooling matter more than a minimal adapter. Stick with SES when AWS IAM and regional controls are already your team's strength. Choose the API-first route when integration effort is the bottleneck and eventual event reconciliation is acceptable.

That boundary matters.

References

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.