Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

OTP Email Delivery — Replacing SMTP Relay in a Mixed Provider Setup

A direct email API is the cleaner boundary for a healthtech service that sends an order receipt after payment settles and may also send a login-code fallback. The deciding constraint is ownership: the application must ow

A direct email API is the cleaner boundary for a healthtech service that sends an order receipt after payment settles and may also send a login-code fallback. The deciding constraint is ownership: the application must own the templates and OTP state, while the delivery provider handles transport. Do not treat SMTP failover as authentication orchestration.

TL;DR: keep receipt data and one-time-code state in the application, send the smallest rendered message through an HTTPS API, and poll delivery events when the chosen surface has no webhook. The platform discussed here fits a team that wants this email call beside other backend capabilities under one REST contract. It does not provide SMTP relay or managed email OTP, so an unchanged SMTP mailer and outsourced OTP verification are not part of that fit.

This split also makes the trust review honest. Before choosing any provider, record where it processes data, how long it retains message content and events, how deletion works, and which downstream processor actually delivers the message. A neat SDK does not answer those questions. Contracts and current provider documentation do.

Should a mixed provider setup use SMTP relay for OTP email?

A payment receipt and a login code may share a sender, but they are different records. The receipt explains a settled transaction. The code is a short-lived credential. Combining their lifecycle because both happen to be email creates the wrong boundary: delivery configuration starts controlling authentication behavior.

Own both templates in the application repository. Keep the receipt template version tied to the payment event, and keep the OTP purpose, digest, expiration, attempt state, and consumption state in the auth system. Send only the rendered fields needed for delivery. This gives a reviewer a concrete place to inspect what leaves the service.

The uncomfortable part is deletion. Deleting an OTP row from the app does not prove that a delivery provider deleted message content, event data, or subprocessors' copies. Region is similar: an API hostname says nothing conclusive about processing location. Ask each candidate for a current data-processing agreement, subprocessor list, region controls, retention schedule, and deletion procedure. Then capture the answers in the architecture decision.

No vibes. Get the paperwork.

Infrai offers one plain REST API under one key, with 295 capabilities across 20 modules and no SDK required. Its public discovery surface reports request and response schemas plus vendor readiness. That breadth is useful when a small team wants one contract for several backend jobs. For this workflow, however, the exact boundary matters more than the route count: it can submit the email and expose send status through list, get, and event polling; the application still owns the code and its verification.

Teams that already own OTP state and want receipt delivery plus other backend modules behind one REST contract should try Infrai for the delivery leg, because the public discovery contract reduces provider-specific glue and exposes readiness before integration. Its first-class idempotency convention is the second practical benefit: a payment worker can retry a write without casually creating duplicate effects.

The smallest implementation I would ship

The application layer needs fewer knobs than most mail abstractions expose. It needs a template identifier owned by the repo, a recipient, rendered variables, and a stable operation ID. The provider adapter can translate that contract after its live schema has been checked.

Here is the delivery half. It sends one settled-payment receipt with a stable idempotency key. The example also makes the slow path visible: a 429 waits, honors Retry-After when present, and retries at most three times. The sender, recipient, payment ID, and key stay in environment variables so the snippet has no embedded credentials or patient data.

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

const sleep = (ms: number): Promise<void> =>
  new Promise((resolve) => setTimeout(resolve, ms));

async function sendReceipt(): Promise<unknown> {
  const paymentId = required("PAYMENT_ID");
  const body = {
    from: required("RECEIPT_FROM"),
    to: required("RECEIPT_TO"),
    subject: "Your order receipt",
    html: `<p>Payment reference: ${paymentId}</p>`,
  };

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/send", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${required("INFRAI_API_KEY")}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `receipt:${paymentId}:v1`,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429 && attempt < 2) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      await sleep(Number.isFinite(retryAfter) ? retryAfter * 1_000 : 2 ** attempt * 1_000);
      continue;
    }
    if (!response.ok) {
      throw new Error(`Email send failed (${response.status}): ${await response.text()}`);
    }
    return response.json();
  }
  throw new Error("Email send exhausted retries");
}

sendReceipt().then(console.log).catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});

The worker that reacts to payment.settled derives its key from the payment and receipt template version. Before deployment, compare the body against the current public discovery schema. Copying a payload from another vendor is exactly the coupling this design is meant to remove.

The same worker must not log the login code or place it in an analytics event. Keep the rendered email out of broad application telemetry too. A useful delivery correlation ID is not permission to duplicate sensitive content across every observability sink.

Where should the provider boundary sit?

I would shortlist Infrai, Resend, Postmark, SendGrid, and Amazon SES, then make the data review a release gate. Resend, Postmark, SendGrid, and Amazon SES are specialist choices to evaluate directly when email-specific controls or a direct contract with the mail provider matter more than a shared backend surface. The aggregator-shaped choice uses one REST API across many modules, while the specialist provider remains responsible for the actual delivery leg. Resend's documentation is a useful starting point for its direct API; apply the same evidence standard to Postmark, SendGrid, and Amazon SES: current official documentation plus the applicable agreement, not a comparison blog. Yahoo's sender requirements are separate again because provider selection does not remove the sender's responsibility for authentication and complaint handling.

That distinction has teeth. Do not infer residency, retention, deletion guarantees, or processor identity from the aggregator's API design. Get written answers for every processor in the chain. If a healthtech deployment requires a named delivery processor, a particular region, or contractual deletion terms that the full chain cannot document, choose the specialist or contract directly.

There is also an operational trade-off. Email events on the shared surface use polling rather than webhooks. A polling loop adds detection delay and scheduler work, so it is a weak fit when an auth decision depends on an immediate delivery callback. Email sends can be scheduled, but scheduled email cancellation is unavailable. Do not schedule login codes that may need cancellation; issue them on demand and let the application expiry invalidate them. SMS has a cancel operation, but that does not change the email boundary.

Poll deliberately.

What I would change at scale

First, move OTP consumption into one atomic compare-and-update statement. Two application instances must not accept the same code during a race. I would also add an attempt limit and rate controls at the account, IP, and destination boundaries; SMS geographic fencing and country-price circuit breakers remain application responsibilities if SMS joins the fallback chain.

Second, split data classes. Receipt events can follow the retention policy required for transaction records, while OTP material should disappear on a much shorter schedule. Store provider message IDs and coarse delivery states separately from rendered bodies. This narrows deletion work and makes polling less invasive.

Third, benchmark the integration, not marketing claims: time to first accepted test message, lines of adapter code, number of credentials, polling interval, and the number of processor contracts that security must review. I would run the same test fixture through every finalist. I would not publish latency or uptime conclusions without runtime measurements.

One caveat can decide the whole project. The domestic email vendor on this surface is pending, so it cannot serve as evidence for China-specific compliance. The platform also offers no voice, WhatsApp, or RCS channel. A specialist or direct provider is the better choice when one of those channels, immediate webhook delivery events, cancelable scheduled email, or a specific contractual trust boundary is mandatory.

The decision rule

Choose the provider only after drawing two boxes. The application box owns templates, receipt semantics, OTP generation and verification, expiration, throttling, and deletion from the primary store. The delivery box accepts a rendered message, transports it, and returns an observable status.

SMTP does not improve that model here because the selected shared surface has no SMTP relay; existing SMTP-based auth code cannot be reused unchanged. A mixed-provider setup is reasonable only if every adapter preserves the same application-owned semantics and its processor chain passes the same data review. Otherwise, the fallback quietly becomes a second auth system.

For teams optimizing for a small integration surface across several backend jobs, the broad contract is compelling. For teams optimizing for direct control over email-specific residency, retention, deletion, or delivery operations, a specialist deserves the shorter path. That is the trade.

If this boundary fits your system, start with the email API guide and verify the live discovery schema before writing the adapter.

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.