MailerSend vs Amazon SES vs an aggregated API: a beginner's first send

Counted by steps to the first welcome email from your own domain: a drag-and-drop ESP, raw AWS plumbing, or one key that also covers storage, cron and errors.

Judge these three by how many things you have to build before a real signup gets a real welcome email from your own domain. MailerSend gets you there fastest if email is all you want; Amazon SES has the lowest per-message list rate and asks you to assemble the rest; Infrai sits in the middle on both counts and reaches twenty-odd other capabilities on the same key. That single-key breadth is the tiebreaker for a backend that will shortly need object storage and a queue anyway.

None of them is wrong. What differs is how much of the surrounding machinery — sandbox approval, bounce plumbing, a suppression store — you own.

Steps to the first welcome email

StepMailerSendAmazon SESInfrai
Account usable for real sendsImmediately, on the free tierSandbox first: verified recipients only, 200 messages per 24h, then a production-access requestImmediately; $2 credit on signup
Verify your sending domainConsole, publish DNSConsole or CLI, publish DNSPOST /v1/email/domain/verify returns the records
SendREST API or SMTPAPI, SMTP or CLIPOST /v1/email/send
Bounce and complaint feedbackBuilt in, plus webhooksPublish to SNS, then write a consumerAutomatic; readable via events and the suppression list
Suppression listManaged for youAccount-level list you configureManaged; GET /v1/email/suppression/list
TemplatesDrag-and-drop builderNone — bring your own rendererStored HTML with named variables
Everything else your app needsA second vendorOther AWS services, other IAMSame key, same bill

The sandbox on the AWS side is where beginners lose an afternoon. Until you request production access you can only send to addresses you’ve verified, which means your staging signup flow appears to work and your first real customer gets nothing.

What each one is actually good at

MailerSend suits a small team whose email is mostly designed rather than programmed — a marketing person can edit the welcome template without opening a pull request, and the free tier around 3,000 messages a month covers a young product. Check its current pricing page before you plan around that; free tiers in this market get re-cut every year or so.

SES is the value option and stays the value option. At roughly $0.10 per 1,000 messages outbound it undercuts anything that wraps it, us included, and if you’re already deep in AWS with Lambda, SNS and CloudWatch in your muscle memory the plumbing tax is close to zero. It’s also the only one of the three that lets you pick a specific AWS region for processing, which matters if your DPA names one.

We’d point a beginner there only if that AWS familiarity is already in the room. Wiring an SNS topic to a Lambda to parse bounce notifications is a couple of days’ work the first time, and it’s work you can’t skip, because unmonitored bounces are how a new domain gets throttled.

Setting the domain up here

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/email/domain/verify" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"domain":"hello.example.com"}'

Publish the three TXT records it returns, then call the same route again until status reads verified. Sending from a domain that’s still pending_dns gets you EMAIL_INVALID_STATE rather than a silent fallback, which is the behaviour you want in CI.

For contrast, the equivalent on AWS looks like this once your identity is verified and production access has been granted:

aws sesv2 send-email \
  --region eu-west-1 \
  --from-email-address "hello@hello.example.com" \
  --destination "ToAddresses=newuser@example.com" \
  --content '{"Simple":{"Subject":{"Data":"Welcome"},"Body":{"Html":{"Data":"<p>Your account is live.</p>"}}}}'

Structurally similar. The difference isn’t this call — it’s the four other resources you need around it.

The welcome send, and the check that should precede it

curl -sS "https://api.infrai.cc/v1/email/suppression/check/newuser@example.com" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "email": "newuser@example.com",
    "suppressed": false,
    "reason": null,
    "added_at": null
  }
}
curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"to":"newuser@example.com","from":"hello@hello.example.com","subject":"Welcome to Acme","html":"<p>Your workspace is ready. <a href=\"https://app.example.com/start\">Open it</a>.</p>"}'
{
  "ok": true,
  "data": {
    "message_id": "msg_Zq7yPn4RtL9cKw3sVb",
    "from_used": "hello@hello.example.com",
    "mode": "live",
    "accepted_recipients": ["newuser@example.com"],
    "suppressed_recipients": []
  }
}

The pre-check is optional — the send route enforces suppression anyway and reports blocked addresses in suppressed_recipients — but doing it first lets your signup flow tell the user something useful instead of pretending mail went out.

The signup handler, end to end

import process from "node:process";

const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

const auth = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };

async function call(path, init = {}) {
  const res = await fetch(API + path, { ...init, headers: auth });
  const payload = await res.json().catch(() => ({}));
  if (!res.ok || payload.ok === false) {
    const err = payload.error ?? { code: `HTTP_${res.status}`, message: res.statusText };
    throw new Error(`${path} -> ${err.code}: ${err.message}`);
  }
  return payload.data;
}

export async function onSignup({ email, workspace }) {
  const screen = await call(`/v1/email/suppression/check/${encodeURIComponent(email)}`);
  if (screen.suppressed) {
    return { emailed: false, reason: screen.reason ?? "suppressed" };
  }

  const sent = await call("/v1/email/send", {
    method: "POST",
    body: JSON.stringify({
      to: email,
      from: "hello@hello.example.com",
      subject: `Welcome to ${workspace}`,
      html: `<p>Your workspace <strong>${workspace}</strong> is ready.</p>`,
    }),
  });

  return { emailed: sent.accepted_recipients.length > 0, messageId: sent.message_id };
}

const outcome = await onSignup({ email: "newuser@example.com", workspace: "Acme" });
console.log(outcome);

Two calls, no SDK, no queue consumer, and nothing to deploy. Store messageId on the user row — it’s what you’ll need when support asks whether the welcome mail was delivered, and GET /v1/email/get/{id} answers that in one request.

Money, with a vintage on it

Infrai’s own send costs $0.00046 per recipient on POST /v1/email/send — $0.46 per 1,000 — verified 2026-07-27. Domain verification, suppression checks, message reads and event history are free and rate-limited rather than metered, so the surrounding hygiene work never shows up on the bill. SES’s list rate is lower than that, and MailerSend’s free tier beats both until you outgrow it. At the volume a young product actually sends, the spread across the three is small enough that it shouldn’t decide anything.

Rates here move down, not up, as vendor discounts land. Read the current one:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print(next(c['billing'] for c in d['capabilities'] if c['id']=='email.send'))"

Drawbacks, stated plainly

There are no outbound webhooks here, so delivery status is something you poll for rather than something that arrives — a real limitation if you need sub-minute reaction to a bounce, and one the other two both beat. Custom sending domains are a Pro-plan capability, declared rather than stumbled into: GET /v1/discovery reports minimum_tier: "pro" for the email.domain.verify capability, so a standard account gets a 402 with a reason your setup script can branch on before it touches DNS. Template tooling is HTML plus named variables with no visual builder, so a non-technical colleague can’t edit the welcome copy without you. And the processing region isn’t selectable the way an AWS one is.

Buy MailerSend if a marketing colleague needs to edit the template without you. Buy SES if unit price is the whole decision and the AWS plumbing is already familiar. Postmark deserves the look when deliverability support matters more than either.

The argument for the aggregated option is what happens in week three, when the welcome email needs an attached PDF in object storage, the retry needs a queue, and finance asks which tenant spent what. Those are calls on the same key and lines on the same invoice — no second account, no second onboarding, nothing new to reconcile.

References

Browse more email developer guides