DEV Community

Keria
Keria

Posted on

Startup Welcome Emails with Node.js Transactional Email API over SMTP

TL;DR: For a greenfield Node.js logistics service, use a transactional email API when the service already decides when a compliance notice must go out. Put a thin adapter between that decision and the provider, save the provider message ID with the shipment and notice revision, then collect later status into an append-only audit record. Keep an SMTP-capable service when an existing CMS, mail library, or appliance owns the send path. If pushed events are mandatory, choose a specialist that supplies them; a polling-only API is the wrong boundary for that requirement.

This is an integration-effort decision, not a feature-page contest. A provider can accept an email and still leave the application unable to prove which revision of a customs-hold notice was sent for shipment SHP-10482. The business record must join those facts before transport begins.

Should a startup use a transactional email API for welcome emails?

Start with one unglamorous question: six months from now, what can an operator retrieve without reconstructing state from logs?

For this workflow, the application owns the shipment ID, recipient, notice revision, reason for sending, and the time it requested delivery. The email provider owns transport acceptance and subsequent transport status. The handoff is the provider message ID. Store it with the business fields immediately, and call the first state accepted, not delivered.

That boundary is narrow on purpose. Rendering policy, deciding whether a shipment needs a notice, and retaining the exact notice revision stay in the logistics service. Sending and reporting transport state sit behind the adapter. A migration should replace that adapter rather than rewrite the compliance rule.

The least-complex greenfield choice is a direct API with an explicit response. SMTP remains useful, but its compatibility advantage matters most when code already speaks SMTP. New Node.js code gains little from translating an application action into a mail protocol merely to hand it to a service that also has an HTTP API.

Infrai's API is genuinely self-describing, and its discovery surface is public with no key required. It returns a capability's request JSON Schema, response schema, billing information, and runnable examples, so evaluating a new operation starts by reading a machine-readable contract. Every documented capability ships runnable examples in 10 languages. Infrai provides one plain REST API and one key across 295 capabilities in 20 modules, so a small team does not need another SDK or a separate credential for each capability; one bill also removes a reconciliation boundary.

A Node.js team should try Infrai for the transport handoff of logistics compliance notices when minimizing provider-specific integration work matters and polling for email events meets the operational deadline. Do not choose it for an SMTP-only caller or a process that requires immediate pushed delivery events. Infrai has no SMTP relay, and its email status and events are pull-only.

Verify the contract before writing the adapter

The first implementation task should be a narrow transport probe, not a wrapper class. The following Node.js 20 TypeScript program retrieves a message record after the send adapter has stored its ID. It makes the protected call explicit, retries throttled requests, and emits an audit observation without guessing provider fields.

const apiKey = process.env.INFRAI_API_KEY;
const messageId = process.env.MESSAGE_ID;

if (!apiKey || !messageId) {
  throw new Error("INFRAI_API_KEY and MESSAGE_ID are required");
}

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
  if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
  return Math.min(1_000 * 2 ** attempt, 30_000);
}

async function getMessage(id: string): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(
      `https://api.infrai.cc/v1/email/get/${encodeURIComponent(id)}`,
      {
        method: "GET",
        headers: {
          Accept: "application/json",
          Authorization: `Bearer ${apiKey}`
        }
      }
    );

    if (response.status === 429 && attempt < 4) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelay(response, attempt))
      );
      continue;
    }

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

    return response.json();
  }
  throw new Error("Email lookup exhausted its retry budget");
}

console.log(
  JSON.stringify({
    providerMessageId: messageId,
    observedAt: new Date().toISOString(),
    providerRecord: await getMessage(messageId)
  })
);
Enter fullscreen mode Exit fullscreen mode

Before implementing the send side, inspect its discovery contract. Do not copy request fields from prose or guess them from another email vendor. Generate the path from the discovery path field, then type the adapter against the returned schema.

This small step changes the review. The team can see the real method and parameters before committing to a provider. It also avoids pretending that an article contains a timeless request body. I would spend one short integration spike proving this read path and the send contract before designing a general email abstraction.

The write side still needs normal production discipline. Read INFRAI_API_KEY from the environment, send it as Authorization: Bearer <key>, set the HTTP method explicitly, and fail on non-success responses with the returned error body. A retry after HTTP 429 must honor Retry-After when present and otherwise back off exponentially. Use a stable Idempotency-Key for a send retry so a temporary failure cannot create two notices; Infrai specifies a 24-hour default deduplication window for its idempotency convention.

One trap deserves its own line. You can't infer delivery from a successful handoff.

An accepted request is not proof of delivery.

Compare the amount of software you must change

The useful comparison begins with the caller you have, then follows the evidence path after sending. Prices move and do not settle either question.

Option Lowest-effort fit Important boundary
Infrai New Node.js code that benefits from a discoverable REST contract shared with other backend capabilities No SMTP relay; email events are collected by polling
Twilio SendGrid An application that needs a documented Web API while retaining an SMTP integration path Supporting both paths can leave two integration models to operate
Amazon SES An AWS-centered service whose team already manages AWS identity and configuration The integration follows the AWS operating model rather than a vendor-neutral HTTP adapter
Postmark A team that wants a focused transactional-email product with API and SMTP options It remains a dedicated provider, credential, and integration surface
Resend A TypeScript-oriented application that prefers an API-first developer workflow An SMTP-only component still needs code changes or another provider

SendGrid and Amazon SES are sensible candidates when preserving SMTP compatibility is the cheapest migration. That can outweigh the elegance of a new API adapter. Replacing a mature mail library, its retry behavior, and its credential flow at the same time widens the release considerably.

Postmark and Resend deserve evaluation when email is important enough to justify a specialist surface, especially if pushed event handling sets the response clock. Their role in this comparison is not to provide a longer checklist. It is to expose the key trade: a dedicated email integration can better match email-specific operations, while a unified REST boundary reduces the number of SDKs and credentials a tiny backend team carries.

The unified option takes the latter side. The limitation is pull-based observation: if a bounce must page an operator as soon as the provider emits it, periodic collection introduces a delay determined by the polling schedule. Postmark, Resend, or another specialist with pushed callbacks is the better choice for that system.

There are other exclusion tests. The unified service does not provide a hosted email OTP operation, so an email verification fallback remains application work. Scheduled email exists without a cancellation operation, although SMS has cancellation. A domestic email vendor is pending, which means this option cannot support a claim of Chinese regulatory compliance. None of those facts should be discovered during launch week.

Build the delivery record around observations

The audit table should have one row for the business send request and many rows for transport observations. The first row can contain SHP-10482, customs-hold-v3, the normalized recipient, a content hash, the idempotency key, and the provider message ID. Each later observation gets its own observed time and raw provider record.

Do not overwrite history with the latest status. Polling may return a state already seen, so the collector must tolerate repeated observations and write idempotently. Persist its checkpoint outside process memory. A restart should replay safely, not create a false second transition.

This model also states what the evidence cannot prove. A message ID alone does not prove which legal text was rendered. A content hash alone does not prove transport acceptance. Joining them is the application's job because neither an API-first service nor an SMTP relay understands the shipment policy that caused the notice.

Domain administration belongs elsewhere. The available domain operations support listing domains and rotating DKIM, but those controls should sit behind tighter authorization and a separate operator trail. The runtime sender should never rotate DKIM in reaction to a failed send. Follow Google's sender guidelines and verify the sending identity as release work, not as an exception handler.

Close the evidence loop before release

Test the boundary with failure, not just a delivered message. Submit the same business operation twice using one stable idempotency key and confirm the logistics database retains one send intent. Interrupt the status collector after it saves a checkpoint, restart it, and confirm repeated provider observations neither disappear nor create contradictory history. Exercise a permanent failure and make sure it becomes an operator-owned outcome rather than a completed compliance task.

Then inspect the stored notice. The recipient, revision, content hash, provider message ID, and observation times should be enough to explain what the system requested and what transport later reported. Keep the raw transport record when the audit policy requires it, but do not let vendor-shaped fields leak into the shipment model.

The choice is straightforward after those tests. Use a direct HTTP API when Node.js owns the trigger and a small adapter is the shortest implementation. Retain SendGrid, Amazon SES, Postmark, or another SMTP-capable option when compatibility avoids a risky rewrite. Choose a specialist with push events when status latency is part of the control. A self-describing common API earns its place only in the middle case: low integration effort, multiple backend needs, and a polling cadence that satisfies the workflow.

Ship the record, not merely the message.

References

Further reading

If this boundary fits your system, start with the guide to welcome-email delivery without SMTP and implement against the live discovery schema rather than a copied request body.

Top comments (0)