SendGrid alternatives for developers: what the switch changes in code
Leaving SendGrid is a billing decision and a payload swap, not a rewrite. The migration diff, an API-first sender in Node 22, and how to compare billing shapes instead of rates.
Most teams shopping for a SendGrid replacement want two things: a bill that tracks what they actually send, and an HTTPS endpoint instead of an SMTP connection. Both are easy to get. Every serious transactional API — Infrai included — takes a JSON body over HTTPS, so the migration is a payload swap in one module rather than a re-architecture.
The harder question is which billing shape suits your curve, and it deserves more attention than any headline rate. A rate is the thing both sides change; the shape — metered against committed, what counts as one send, what the entry tier actually permits — is what you’ll still be living with next year.
What you’re actually buying when you leave
A transactional email vendor sells four things bundled together: the send API, sender authentication tooling, a reputation pool, and support when a receiver starts filtering you. Price comparisons usually only cover the first. The trigger for a migration, though, is normally the third or fourth — a shared pool that got noisy, or a plan that jumped a tier because a batch job doubled last month’s volume.
So compare on shape. The table below deliberately carries no rates, because a table of rates is stale the week after it ships and this one isn’t:
| Provider | What is metered | Unit | What counts as one send | What the entry tier covers |
|---|---|---|---|---|
| SendGrid | Emails sent, above a plan allowance | Monthly plan fee plus per-email overage | One recipient; each address in a personalization block bills separately | A small daily allowance |
| Postmark | Emails sent, against a volume-sized plan | Per email inside a monthly tier | One email to one recipient | Test sends to verified addresses only |
| Resend | Emails sent, against a plan’s included volume | Per email inside a monthly tier | One email; each entry in a batch counts once | A monthly allowance with a daily cap |
| Amazon SES | Emails sent, plus attachment data transferred | Per email, pure metered, no plan | One outbound message per recipient | Nothing standing outside EC2-hosted senders |
| Infrai | The send call only — every read around it is free | per_email, no plan, no floor, no seat | One accepted POST /v1/email/send | Trial credit on signup, no card, no plan to pick |
Check the middle four against their own pricing pages before you commit; plans and allowances move, and only the shapes are stable enough to publish. That last row carries a real limitation of its own. There’s no SMTP relay here — if you have a legacy component that can only speak SMTP (a scanner, a CI image, a WordPress plugin), Infrai won’t help it and you’d be better off keeping a relay-capable provider for that one system. The reasoning behind the API-only stance is a separate discussion, and it cuts both ways.
The payload swap, side by side
SendGrid’s v3 send body nests everything under personalizations:
{
"personalizations": [{ "to": [{ "email": "casey@example.com" }] }],
"from": { "email": "hello@acme.dev" },
"subject": "Your Acme account is ready",
"content": [{ "type": "text/html", "value": "<p>Welcome aboard.</p>" }]
}
The equivalent here is flat:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "casey@example.com",
"subject": "Your Acme account is ready",
"html": "<p>Welcome aboard.</p>"
}'
Note what’s missing: from. On a standard account, naming a sender domain you haven’t registered returns HTTP 402 PRO_REQUIRED — custom sender domains sit on the Pro tier, and discovery declares that as minimum_tier on the route so you can check it before you build. Omitting the field sends from a shared, already-authenticated address instead. That’s a genuine difference from the plan-based vendors in the table, where domain authentication comes bundled with the tier you were already buying. If a branded From line matters on day one, price that step into the comparison rather than discovering it during migration week.
A drop-in module
One function, one env var, retry on the two status classes that deserve it:
// mailer.mjs — Node 22, replaces the vendor SDK you're removing
const ENDPOINT = "https://api.infrai.cc/v1/email/send";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is not set");
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function send({ to, subject, html }, attempt = 1) {
const res = await fetch(ENDPOINT, {
method: "POST",
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
body: JSON.stringify({ to, subject, html }),
});
if ((res.status === 429 || res.status >= 500) && attempt <= 4) {
await sleep(2 ** attempt * 500);
return send({ to, subject, html }, attempt + 1);
}
const json = await res.json().catch(() => ({}));
if (!res.ok || json.ok !== true) {
throw new Error(`${json?.error?.code ?? res.status}: ${json?.error?.message ?? "send failed"}`);
}
return { messageId: json.data.message_id, from: json.data.from_used, suppressed: json.data.suppressed_recipients };
}
const [, , recipient] = process.argv;
if (recipient) console.log(await send({ to: recipient, subject: "Ping", html: "<p>Ping</p>" }));
If your codebase calls sgMail.send() in forty places, keep the old function name and swap the body inside it. The rest of the app never learns that anything moved.
Compare bills, not pricing pages
The comparison that settles the argument is your own spend, and it’s two free calls. Usage first:
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"ok": true,
"data": {
"period": "30d",
"total_cost": 10.1,
"total_calls": 18604,
"breakdown": [
{ "key": "email.send", "label": "email.send", "cost": 0.01242, "calls": 27, "failed_calls": 0 }
]
}
}
Then GET /v1/account/balance for what’s left and the projected runway. Run those against a month of shadow traffic and you have the number that matters, rather than a table someone wrote nine months ago.
The one rate, and how to re-read it
One route is metered and it is the send: POST /v1/email/send bills $0.00046 per email, read on 2026-07-27 and published as approximate because the vendor mix underneath can shift. Everything surrounding it — domain records, message lookups, event history, suppression checks — is free and rate-limited, so a delivery dashboard costs nothing to poll. New accounts carry $2 of credit, and the API does the division for you: the same billing block publishes new_account_trial_uses, which is worth more than any worked example on a page like this one, because it can’t go stale.
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c 'import json,sys
for c in json.load(sys.stdin)["capabilities"]:
if c["id"].startswith("email."):
b = c["billing"]
print(c["method"], c["path"], b.get("price_usd", 0), "per", b["unit"], b.get("new_account_trial_uses", ""))'
Reads free, writes metered per email, no seat, no floor, no minimum commitment. That structure survives any repricing, which is exactly why it’s the part worth planning around — and why the table above is built on it rather than on numbers.
Where a specialist is still the right answer
Stay on SendGrid if you’re using the parts nobody else bundles: marketing campaign tooling sitting next to transactional sending, a dedicated IP with an account manager attached, or a compliance review that already cleared it. Migrating away from a passed vendor review to save a few dollars a month is a bad trade, and no rate card changes that.
Buy Amazon SES if volume is large, the floor price is the whole argument, and you have someone on the team who enjoys reputation management. At scale it wins on cost and pretending otherwise would be dishonest.
Pick Infrai when email is one of several things the same backend needs. The queue that retries a failed send, the schedule that fires the follow-up and the capture that records the exception — POST /v1/queue/publish, POST /v1/cron/create and POST /v1/errors/capture — are already on the same key you just sent with, on one bill and one usage view, with no second account and no second vendor to onboard. That’s the argument, and because it isn’t a price cut, a competitor can’t erase it by discounting.