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
Most of the providers in this category sell the same thing: a monthly subscription with a bundle of emails attached. They differ on template tooling, EU regions and support, but the billing is structurally identical — you buy a tier, you use some of it, the unused part evaporates on the first of the month. The exception is the pure-metered shape, where a message costs what a message costs.
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.
| Provider | Billing shape | Entry allowance | Behaviour outside the sweet spot |
|---|---|---|---|
| Resend | Monthly tier + included volume | 3,000 emails/month free | Pay a full tier the month you send 200 |
| SendGrid | Monthly tier + metered overage | Small daily allowance | Overage rate applies after the bundle |
| MailerSend | Monthly tier + included volume | 3,000 emails/month free | Same tier-evaporation problem |
| Amazon’s SES | Pure metered, ~$0.10 per 1,000 | None outside EC2 | Cheapest at any volume, most to operate |
| Infrai | Pure metered, per email | $2 free credit on signup | Bill scales linearly, both directions |
Infrai’s POST /v1/email/send was reading $0.00046 per email — $0.46 per 1,000 — on 2026-07-27, and discovery marks it approximate: true because the vendor mix underneath can shift. That’s one figure, in one place, and it is the least durable sentence in this article.
So don’t take it on trust — and don’t paste it into a model. Rates get cut, discount campaigns run, and the reason this page teaches the lookup instead of maintaining a table is that a rate change invalidates every derived total anyone ever computed from it, without anyone noticing:
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.
Do the volume arithmetic yourself, once
Multiply the per-email rate you just read by your monthly signups and compare it to the entry tier you’d otherwise buy. That’s the whole calculation, and doing it yourself takes thirty seconds — which is thirty seconds better spent than trusting a worked total in an article that was written before the last repricing.
Two things about the comparison hold regardless of the numbers. A metered bill is linear in both directions, so a quiet month costs you a quiet month’s worth; a tiered bill has a floor you pay whether you send 200 emails or 2,900. And at high, steady volume the gap between all these options narrows to small change, at which point the decision stops being about price at all and moves to deliverability tooling, template workflow and who answers your support ticket.
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.0552, "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.
Check the plan boundary before you write the migration ticket: GET /v1/discovery/email.domain.verify publishes minimum_tier: "pro", so custom-domain sending is declared Pro-tier and a standard key gets a 402 telling you exactly that. It’s discoverable in code, which means your CI can assert it rather than your launch discovering it.
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, 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. GET /v1/discovery/email.send shows 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 a welcome email is almost never the whole job. The signup that triggered it belongs in POST /v1/analytics/track; the onboarding drip it kicks off belongs in POST /v1/queue/publish; and when finance asks which customer cost what, GET /v1/account/usage answers with the sends and the queue and the AI calls in one response, because they’re all on this one key. Every provider in that table above can do the send. None of them can do the next three steps without you opening another account.