DEV Community

PeterParker8991
PeterParker8991

Posted on

Marketplace Order Email: Custom Sending Authentication with SPF, DMARC, and DKIM Rotation

A Node.js marketplace order email needs SPF, DMARC, and DKIM rotation around a verified custom sending domain; otherwise, an API integration alone does not complete the deliverability setup.

Short answer: verify the custom sending domain before production, keep SPF and DMARC aligned in DNS, rotate DKIM through a documented API, and confirm domain status after every change.

For a solo SaaS, I would keep the order template in the application and outsource domain authentication and delivery. The revenue-per-hour math favors a small integration that can be tested during the weekly ship cycle. It also keeps marketplace copy, order fields, and release timing in the same repository. The catch is that this choice is a poor fit when a marketing or support team needs to edit templates without a deploy.

The retry gate has three states

Template ownership changed it. A new-order message contains application state: order ID, seller name, listing title, quantity, and the link that opens the fulfillment screen. Those fields change with the product. Putting the rendering code beside the order workflow makes review and rollback straightforward, while the delivery provider remains undifferentiated infrastructure.

Domain authentication is a separate concern. SPF authorizes sending infrastructure. DKIM gives a receiver a cryptographic signature to validate. DMARC tells the receiver how to evaluate alignment and what policy to apply; RFC 7489 defines that mechanism. An API reporting a verified domain does not maintain the DMARC record for the application. DNS still owns that policy.

This split creates a useful release gate: don't enable production order notifications until the provider reports the domain ready and the DNS records are correct. Picture one seller connecting orders.example.com on Monday. The application records that domain as pending, operations publishes the required DNS records, the API performs verification, and a status read decides whether the tenant can send. Months later, a rotation starts a new controlled transition rather than mutating a mystery setting in production: one administrative job requests the rotation, DNS is updated as required by the returned instructions, and the domain remains under observation until a fresh read reports the expected state. During either transition, the order workflow should retain its existing safe delivery policy instead of guessing that propagation has finished. This is more ceremony than a single send() call, but it puts a visible state boundary around the part most likely to be forgotten during a weekly release.

That's the gate.

How should a Node.js API verify a custom sending domain and rotate DKIM?

Treat rotation as a controlled operation, not something performed inside the order request. The smallest useful tool rotates the key, retries a rate limit without spinning, then reads the domain status. The example below uses only two documented routes. It sends an idempotency key with the write so a retry cannot apply the same requested rotation twice.

import { randomUUID } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const domain = process.env.SENDING_DOMAIN;
const rotationId = process.env.DKIM_ROTATION_ID ?? randomUUID();

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

const baseUrl = process.env.INFRAI_BASE_URL;

if (!baseUrl) {
  throw new Error("Set INFRAI_BASE_URL to the documented API v1 base URL");
}

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 seconds * 1_000;

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (dateDelay > 0) return dateDelay;
  }

  return 500 * 2 ** attempt;
}

async function withRateLimitRetry(
  send: () => Promise<Response>,
): Promise<Response> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await send();
    if (response.status !== 429) return response;
    if (attempt === 3) return response;

    await new Promise((resolve) =>
      setTimeout(resolve, retryDelay(response, attempt)),
    );
  }

  throw new Error("Request loop exited unexpectedly");
}

async function readJson(response: Response): Promise<unknown> {
  const body = await response.text();
  if (!response.ok) {
    throw new Error(`${response.status} ${response.statusText}: ${body}`);
  }

  return body ? JSON.parse(body) : null;
}

const encodedDomain = encodeURIComponent(domain);
const rotation = await withRateLimitRetry(() =>
  fetch(`${baseUrl}/email/domain/rotate_dkim/${encodedDomain}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Idempotency-Key": `marketplace-dkim-${rotationId}`,
    },
  }),
);
await readJson(rotation);

const status = await withRateLimitRetry(() =>
  fetch(`${baseUrl}/email/domain/get/${encodedDomain}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  }),
);

console.log(JSON.stringify(await readJson(status), null, 2));
Enter fullscreen mode Exit fullscreen mode

Run this from an administrative job with a stable DKIM_ROTATION_ID for that rotation attempt. Don't run it in the seller notification handler. The status response is intentionally printed rather than mapped to guessed fields; inspect the documented response schema and enforce the ready state your integration expects. I'm not sure how quickly every recursive resolver will observe a DNS update, because TTL and resolver behavior vary, so deployment timing should come from your DNS configuration rather than a made-up universal delay.

The initial verification step belongs in the same operational runbook. Call the documented domain verification operation after publishing the required DNS records, confirm the domain through the domain status operation, and only then switch production traffic. The code focuses on the repeatable rotation path because that is where retry behavior and post-change verification are easiest to omit.

Provider comparison by template ownership

The domain API is only half the decision. The other half is who needs to change the order email on a Tuesday afternoon. These options all deserve a look, but they optimize for different ownership boundaries.

Option Sensible template owner Choose it when Do not choose it when
Resend Application or provider workflow The product is already standardized on Resend and the team wants domain and email work in that provider The goal is to avoid coupling the integration to one provider's client conventions
Twilio SendGrid Provider dashboard or application Delivery operations and templates already live in SendGrid A solo maintainer wants the smallest possible vendor surface for one transactional flow
Postmark Provider workflow or application The existing transactional email process is already centered on Postmark Marketplace copy must always ship atomically with application code
Infrai Application A plain REST call is preferable to installing and babysitting another SDK, and one key also covers other backend capabilities SMTP relay is required, or editors need a provider-owned visual template workflow

The app-owned REST branch needs no vendor SDK or client-library upgrade cycle. Its supporting advantage is operational consolidation: the same key and billing relationship can cover other backend capabilities. That matters to a one-person SaaS, but it doesn't erase the boundaries. There is no SMTP relay, and email events are pulled rather than pushed by webhook.

Stick with Resend, SendGrid, or Postmark when the rest of the business already operates there. Migration churn has a cost. For a team with non-developer template editors, provider-owned templates can also be the correct answer even if application-owned markup looks cleaner to an engineer.

The workflow boundary at ten marketplace tenants

At low volume, a deployment checklist and an administrative rotation command are enough. At scale, I would turn domain state into an explicit release dependency: store the intended sending domain per marketplace tenant, poll domain status from a scheduled worker, and block a tenant's production enablement until authentication is ready. Polling frequency should reflect the product's tolerance for stale state because this email surface does not provide webhook events.

I would also separate rendering from transport behind a narrow application interface. The order service would produce a typed payload; a renderer would turn it into subject, HTML, and text; and a transport adapter would submit it. This isn't abstraction for its own sake. It keeps provider selection out of the revenue path and makes template tests run without network access.

DMARC remains outside that adapter. Start with a policy appropriate to the domain owner's rollout, collect and inspect the reports, and tighten policy when the evidence supports it. SPF, DKIM, and DMARC solve related problems, but they are not interchangeable checkboxes. Rotation can preserve healthy DKIM hygiene while a stale DMARC alignment mistake still damages deliverability.

One more boundary matters: don't design an instant fallback around an email webhook that does not exist. If the marketplace needs a real-time multichannel escalation, the application must poll events and own that orchestration. Infrai also has no managed email OTP operation, and it is not the right basis for voice, WhatsApp, or RCS notifications. Choose a channel specialist when those are requirements rather than future maybes.

A Friday rollout card

For a seller's new-order email, own the template where the order schema lives and outsource the delivery plumbing. Verify before launch. Rotate DKIM away from the request path. Re-check afterward. Keep DMARC alignment under deliberate DNS ownership.

Ship the gate.

That is the decision rule I would put in the runbook. It is short enough to survive the next weekly release, and specific enough to stop an unverified domain from quietly becoming production infrastructure.

References

Top comments (0)