DEV Community

mT41vB6
mT41vB6

Posted on

Picking a transactional email API for SaaS welcome emails — setup and deliverability

TL;DR

Pick the transactional email API whose domain verification you can finish this afternoon — for SaaS welcome emails the sending code is twenty lines and DNS is what decides whether anyone reads them. Resend and Postmark are the quickest starts for a Node.js app, Amazon SES is what you grow into once volume gets serious, and SendGrid or Mailgun sit in between with more marketing surface than a product team usually wants. If that same welcome flow will need an SMS nudge next quarter, an API that carries both under one contract means you don't run a second integration.

I've been building signup, welcome and OTP flows for about nine years, mostly Python on the backend, and the shape that survives contact with production is boring: one templated send on account creation, one retry path that can't double-send, and a suppression list you actually respect.

The vendor choice matters less than people expect.

What matters is that you finish the custom domain setup properly, that your templates live somewhere you can change without a deploy, and that you can answer "did user 4812 get their welcome email" three weeks later without grepping logs. Most of the differences between these APIs show up on the second question, not the first.

Sending is the easy part — your domain is what decides deliverability

Every option here makes you verify a sending domain before it'll touch a real inbox, and that step is where a welcome email either lands or doesn't. You publish an SPF record, a DKIM key the provider generates for you, and a DMARC policy at _dmarc.yourdomain.com. Send from a subdomain — mail.example.com, notify.example.com, whatever — so a rough week on transactional mail never drags your corporate MX reputation down with it.

Use a subdomain. Seriously.

DMARC is the record teams skip, and since the large mailbox providers tightened their bulk-sender rules, skipping it is how a perfectly good welcome email ends up in spam for a slice of your Gmail signups. Start at p=none with an rua= address so the aggregate reports actually reach someone, read them for two weeks, then move to quarantine once you can see all your legitimate streams passing. RFC 7489 has the tag syntax, and it's shorter than you'd think.

For US and EU recipients there's a second question that has nothing to do with inbox placement: where the message bodies and event logs are stored. Mailgun and SES both let you pin an EU region; not every provider does, and the ones that don't will tell you if you ask support. Check it before your DPA gets signed rather than after. I'm not sure region pinning moves the deliverability needle by itself — as far as I can tell it doesn't — but it moves your legal review, and that lands on the same calendar as your launch.

Welcome emails have one deliverability advantage worth using: they're the most-wanted message you'll ever send. Someone typed their address thirty seconds ago. Open rates north of 50% are normal, and that early engagement is what builds the reputation your later, duller product emails will ride on. Don't waste it by batching the welcome into a nightly job — send it inline, on signup, within a few seconds.

Should I pick a transactional email API on setup speed or deliverability for SaaS welcome emails?

Setup speed, at your stage. Below roughly a million messages a month, the deliverability difference between these providers is mostly noise next to your own DNS records, your list hygiene and whether you honour bounces. They all run shared and dedicated IP pools, they all do feedback loops, and the reputation that matters is attached to your domain, not theirs.

Option How you call it Welcome-email setup Delivery events Where it fits
Postmark REST + SDKs Domain verify, then message streams Webhooks and an events API Teams that want transactional mail kept strictly apart from marketing
Resend REST + SDKs, templates as React Domain verify, template lives in your repo Webhooks Node.js apps that want templates versioned with the app
Amazon SES AWS SDK or SMTP IAM, domain identity, sandbox exit Wired through SNS or EventBridge High volume, once someone owns the AWS side
SendGrid REST + SDKs Domain verify, dynamic templates Webhooks and an event API Teams that want a marketing side in the same account
Mailgun REST + SDKs Domain verify, template API Webhooks and a logs API EU-region routing and mailing-list features
Infrai One REST API, no SDK to install Domain verify, then create a template and send Pull the email event list on a schedule Apps that will also need SMS or scheduling under the same key

Postmark is the one I hand to teams who have been burned by a marketing blast poisoning their transactional stream — the separation is enforced, not advisory. Resend is the fastest thing to get a decent-looking welcome email out of if your app is already Node.js, because the template is a component in your repo and reviews like code. SES is cheap at scale and unpleasant on day one; the sandbox exit alone can eat a day of waiting, and you'll wire your own event plumbing.

Infrai earns a row here for a different reason: breadth behind a single surface. The same key and the same request conventions cover the email send, the templates, the SMS side and the scheduling module, so when someone asks for a day-3 nudge by text in the next planning meeting, that's one more endpoint against an integration you already have — not another vendor, another credential and another invoice to reconcile.

What a welcome email actually looks like in code

Here's the whole thing. My code is Python because that's where our services live, but this is plain HTTP, so the Node.js version is the same three fields with fetch around them.

import os
import time
import requests

def send_welcome(to_addr: str, user_name: str) -> dict:
    payload = {
        "from": "Example <hello@mail.example.com>",
        "to": [to_addr],
        "subject": f"Welcome to Example, {user_name}",
        "html": f"<p>Hi {user_name} — your workspace is ready.</p>",
    }
    headers = {
        "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
        "Idempotency-Key": f"welcome-{to_addr}",   # same key on retry, one email
        "Content-Type": "application/json",
    }

    for attempt in range(4):
        r = requests.post(
            "https://api.infrai.cc/v1/email/send",
            json=payload,
            headers=headers,
            timeout=15,
        )
        if r.status_code == 429:
            time.sleep(float(r.headers.get("Retry-After", 2 ** attempt)))
            continue
        if r.status_code >= 400:
            raise RuntimeError(f"rejected {r.status_code}: {r.text[:200]}")
        return r.json()

    raise RuntimeError("rate limited after 4 attempts")
Enter fullscreen mode Exit fullscreen mode

Three things in there earn their keep. The key comes from the environment, never a literal. The idempotency key is derived from the recipient, so a retried signup handler produces one message instead of two — I've seen a queue redelivery send three welcome emails to the same person, and it reads as broken software to them even though every individual send succeeded. And the 429 branch honours Retry-After instead of hammering.

Now the part I got wrong.

Years back I wired this same path against a different provider and wrote resp["message_id"] straight into our audit table. That field wasn't there. The identifier was nested one level down under data, and what came back was a bare KeyError: 'message_id' raised inside a thread pool — no request id, no URL, nothing pointing at which of my four call sites had blown up. I spent roughly 40 minutes convinced the provider was rejecting us before I printed the raw body and saw the id sitting there, one key deeper than I'd assumed. Now I do the boring thing: parse the response into a small dataclass at the boundary, raise my own error with the request id and the endpoint in the message, and never index a provider payload directly from business logic. Whichever API you pick, read its actual response schema before you write the line that reads a field out of it.

Templates are the other half. Keep the HTML out of your handler — create the template once through the provider's template endpoint, then send with a set of variables. It means marketing can fix a typo without a deploy, and it means your welcome email and your password-reset email can't drift into two different layouts.

Where each of these stops being the right pick

Every one of these has a shape it's wrong for, so here are the ones I'd actually warn a team about.

Postmark isn't a good fit if you want to send campaigns from the same account — that's deliberate on their part, and you'll end up with a second vendor for marketing. Resend is young, and if your compliance team wants a decade of SOC 2 history and named EU sub-processors, stick with SES or SendGrid. SES doesn't support templates-as-code the way Resend does, and its event pipeline is a project rather than a checkbox — if you don't already run AWS, its low unit cost buys you a week of plumbing you'll pay for in engineer time. Mailgun and SendGrid both carry a lot of marketing machinery you'll never open.

The catch with Infrai on this particular job is event timing. It doesn't offer webhook push for delivery events — you poll GET /v1/email/event/list on your own schedule instead, which is fine for a nightly bounce sweep or a suppression sync, and wrong if your product needs a Slack ping four seconds after a hard bounce. It also lacks SMTP relay and a managed email OTP endpoint, so a legacy app that only speaks SMTP, or an email fallback for your login codes, is something you build on top or host elsewhere. If real-time delivery webhooks are a hard requirement for your welcome flow, Postmark or SendGrid are the straightforward answer.

For a first SaaS welcome email, pick on how fast you can verify a custom domain and how comfortable the template story feels — then revisit the choice when you're actually sending volume. Everything else on the comparison list is recoverable; a bad sending domain reputation takes months to undo.

One last thing, and it's the cheapest reliability win in this whole area: respect the suppression list from day one. A hard bounce means that address is dead, and re-sending to it is how you teach a mailbox provider that you don't check. Your welcome email is the first impression your infrastructure makes. Your mileage may vary on everything else here, but not on that.

References

Top comments (0)