Cheapest transactional email API for SaaS welcome emails (Node 22)

Metered per-send billing versus monthly plans across Resend, Postmark, SendGrid and MailerSend, with a runnable Node 22 welcome sender on Infrai.

A welcome email is one send per signup. It fires on a user action, it never batches, and its volume tracks your growth curve instead of a campaign calendar — which is exactly the shape that makes monthly email plans a bad deal early on. Metered per-send billing wins below the point where a plan’s included volume gets used up. Infrai meters every email individually, with no monthly floor and no seat.

The rate itself is the least interesting part of this comparison, though, so start with the shape of the bill rather than the number on it.

Plans versus meters, and which one your signup curve wants

Four of the five providers named in this query sell the same thing: a monthly subscription with a bundle of emails attached. Resend, Postmark, SendGrid and MailerSend differ on template tooling, EU regions and support, but their billing is structurally identical — you buy a tier, you use some of it, the unused part evaporates on the first of the month. Amazon SES is the outlier that charges strictly per message.

Free allowances and tier prices move around, so treat the table as shape rather than gospel and open the vendor’s own pricing page before you commit.

ProviderBilling shapeEntry allowanceBehaviour outside the sweet spot
ResendMonthly tier + included volume3,000 emails/month freePay a full tier the month you send 200
PostmarkMonthly tier sized by volume100 test sends/monthNo perpetual free tier; per-tier jumps
SendGridMonthly tier + metered overageSmall daily allowanceOverage rate applies after the bundle
MailerSendMonthly tier + included volume3,000 emails/month freeSame tier-evaporation problem
Amazon SESPure metered, ~$0.10 per 1,000None outside EC2Cheapest at any volume, most to operate
InfraiPure metered, per email$2 free credit on signupBill scales linearly, both directions

Infrai’s POST /v1/email/send costs $0.000115 per email — that’s $0.115 per 1,000, verified 2026-07-26 and marked approximate because the vendor mix underneath can shift. Against SES’s list rate you’re paying roughly 15% more per thousand for not running SES yourself. The $2 credit a new account starts with covers about 17,391 sends, which is more welcome emails than most products send in their first year.

Don’t take that figure on trust. Rates get cut, discount campaigns run, and what you read today may well be lower than what’s printed here:

curl -s https://api.infrai.cc/v1/discovery \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  | python3 -c 'import json,sys
d=json.load(sys.stdin)
for c in d["capabilities"]:
    if c["id"].startswith("email."):
        b=c.get("billing",{})
        print(c["method"], c["path"], b.get("price_usd","free"), b.get("unit",""))'

Every other email route in that listing prints free. Domain verification, template management, suppression lists and event history are not metered — only the send is.

Three volumes, one arithmetic

At 2,000 welcome emails a month you’d pay about $0.23 metered. At 30,000, roughly $3.45. At 300,000, around $34.50. Compare that against a $20–$30 monthly plan: the plan is worse at the first volume, comparable at the second, and better only if your provider’s tier happens to bracket your actual send count tightly.

The honest reading is that at high, steady volume the difference between all six options is small change, and the decision moves to deliverability tooling and support. At low or spiky volume, metering is simply cheaper because you can’t overpay for an empty bundle.

Your own spend is a call, not a spreadsheet:

curl -s "https://api.infrai.cc/v1/account/usage" \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"
{
  "ok": true,
  "data": {
    "period": "30d",
    "total_cost": 9.12410321,
    "total_calls": 16039,
    "breakdown": [
      { "key": "email.send", "label": "email.send", "cost": 0.0138, "calls": 120, "failed_calls": 0 }
    ]
  }
}

Prove you own the sender domain first

You can’t send welcome mail from hello@yourapp.com until the domain passes verification. One POST returns the DNS records to publish; publish them, then call it again until the status flips.

curl -s -X POST https://api.infrai.cc/v1/email/domain/verify \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}'
{
  "ok": true,
  "data": {
    "domain": "example.com",
    "domain_id": "dom_bf6PJQEPtmPrI0UfspscoWpF",
    "status": "pending_dns",
    "dns_records": [
      { "type": "TXT", "name": "example.com", "value": "v=spf1 include:_spf.infrai.cc ~all", "purpose": "spf", "ttl_recommended": 3600 },
      { "type": "TXT", "name": "cf._domainkey.example.com", "value": "v=DKIM1;k=rsa;p=MIIBIj...AB", "purpose": "dkim", "ttl_recommended": 3600 },
      { "type": "TXT", "name": "_dmarc.example.com", "value": "v=DMARC1;p=none;rua=mailto:dmarc@infrai.cc", "purpose": "dmarc", "ttl_recommended": 3600 }
    ],
    "warm_up_state": "not_started",
    "daily_limit_current": 50000,
    "daily_limit_target": 500000
  }
}

Listing your domains needs no path parameter, so it’s the quickest way to confirm a key works at all:

curl -s https://api.infrai.cc/v1/email/domain/list \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"

Store the copy as a template, not a string literal

Marketing will rewrite your welcome copy. Keeping it in a template means that edit doesn’t need a deploy, and template_vars keeps the per-user substitution out of your handler.

curl -s -X POST https://api.infrai.cc/v1/email/template/create \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "welcome_v3",
    "subject": "Welcome to {{app}}, {{name}}",
    "html": "<p>Hi {{name}} — your {{app}} workspace is ready.</p>",
    "variables": ["name", "app"]
  }'

Render it before a human ever receives it. A non-empty missing_vars in the preview response means a variable never got supplied, which is how Hi , reaches production:

curl -s -X POST https://api.infrai.cc/v1/email/template/preview/tpl_welcome_v3 \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  -H "Content-Type: application/json" \
  -d '{"vars": {"name": "Dana", "app": "Acme"}}'

The sender, in Node 22

No SDK to install — fetch has been built into Node since 18, and this runs on 22 with zero dependencies. The suppression check before the send is the part people skip; sending to an address that already hard-bounced costs you money and reputation at the same time.

// welcome-email.mjs — Node 22, no dependencies
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

async function call(path, { method = "GET", body } = {}) {
  const res = await fetch(`${API}${path}`, {
    method,
    headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const payload = await res.json();
  if (!res.ok || payload.ok === false) {
    const err = payload.error ?? { code: `HTTP_${res.status}`, message: res.statusText };
    throw new Error(`${err.code}: ${err.message}`);
  }
  return payload.data;
}

export async function sendWelcome({ email, name, templateId }) {
  const screen = await call(`/v1/email/suppression/check/${encodeURIComponent(email)}`);
  if (screen.suppressed) return { skipped: true, reason: screen.reason };

  const sent = await call("/v1/email/send", {
    method: "POST",
    body: {
      to: email,
      from: "hello@example.com",
      template_id: templateId,
      template_vars: { name, app: "Acme" },
    },
  });
  return { skipped: false, messageId: sent.message_id, accepted: sent.accepted_recipients };
}

const out = await sendWelcome({
  email: process.argv[2] ?? "new.user@example.com",
  name: "Dana",
  templateId: "tpl_welcome_v3",
});
console.log(JSON.stringify(out, null, 2));

Run it with INFRAI_API_KEY=your_infrai_api_key node welcome-email.mjs dana@example.com. A failure surfaces as a documented error code — EMAIL_NOT_CONFIGURED means the sender domain isn’t verified yet, which is the most common first-run stumble.

Confirm it landed before you close the ticket

The message_id from the send is the handle for everything after it. Poll the message for a state, or list recent sends when you just want to see the last few:

curl -s "https://api.infrai.cc/v1/email/list?limit=3" \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"

That call, and the per-message read behind it, cost nothing.

Where a specialist beats this

If email is the only outbound thing your product does and you send millions a month, Amazon SES is cheaper than any reseller and you should stick with it — the trade-off is that you own the bounce plumbing, the IAM policy and the sandbox exit. If your team lives in a template editor and wants the best-documented deliverability support in the category, Postmark is worth its premium. Infrai’s email send currently routes through one ready vendor, with others pending, so a provider-level failover story is thinner here than at a dedicated ESP, and there’s no inbound-parse route at all.

What tips it for a small SaaS is that the same key also reaches storage, queues, cron, error tracking and AI inference. The welcome email is rarely the whole job — you also want the signup event captured and the onboarding job queued, and those don’t need a second vendor, a second invoice or a second key rotation.

References

Browse more email developer guides