Email or SMS for SaaS alerts: what a delivered notification costs
A delivered SMS costs roughly 65x a delivered email. The routing rule that follows from that, the live price lookup, and runnable send-and-track code for both.
For SaaS alerting, email is the default and SMS is the exception you pay for on purpose. On Infrai both live behind the same key: POST /v1/email/send bills per email, POST /v1/sms/send bills per message, and the gap between the two units is about two orders of magnitude. So the question worth answering isn’t which vendor prints the smallest number — it’s which of your events actually deserves a phone.
Get that split right and the provider choice stops mattering much. Get it wrong and no discount saves you, because you’re paying carrier rates to tell someone their weekly report is ready.
The two units you’re really comparing
Email is priced per message with essentially no floor. SMS is priced per segment — 160 GSM-7 characters, or 70 if you slip a single emoji or curly quote into the body — and in the US the base rate is only half the bill, because carrier surcharges are added on top of whatever your provider charges.
That surcharge is the part comparison tables tend to drop. Twilio publishes $0.0083 per outbound US segment and then adds a per-carrier fee of $0.0035 to $0.005, so an AT&T delivery lands near $0.0118. Plivo publishes $0.0077 for long and short codes with the same carrier surcharges bolted on. Neither of those is a bad deal; they’re just not the number on the pricing page.
Infrai’s SMS route is $0.007475 per message and its email route is $0.000115 per email, both verified 2026-07-25 and both quoted as the final per-call figure rather than a base rate plus fees. Read today’s values yourself — every route’s billing block is in the discovery document:
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-o discovery.json
node --input-type=module -e '
import { readFileSync } from "node:fs";
const doc = JSON.parse(readFileSync("discovery.json", "utf8"));
const caps = doc.data?.capabilities ?? doc.capabilities ?? [];
for (const c of caps) {
if (c.id === "sms.send" || c.id === "email.send") {
console.log(c.id, c.billing.price_usd, c.billing.unit);
}
}'
New accounts start with $2 free credit, which is about 267 messages on the SMS side or 17,391 on the email side — the same $2, a 65x difference in how far it goes. Rates in this market move down rather than up, so what you read from that call may well be lower than what’s printed here.
| Channel | Provider | Published unit | Carrier fee on top? |
|---|---|---|---|
| SMS (US) | Twilio | $0.0083 per segment | Yes, $0.0035–$0.005 |
| SMS (US) | Plivo | $0.0077 per segment | Yes, same surcharges |
| SMS | Infrai sms.send | $0.007475 per message | No separate line |
| Resend | $20 / 50,000 (overage $0.90 per 1,000) | n/a | |
| Postmark | $15 / 10,000 (overage $1.80 per 1,000) | n/a | |
Infrai email.send | $0.000115 per email ($0.115 per 1,000) | n/a |
The email row is where per-message pricing stops being the interesting variable. At 10,000 alerts a month you’re arguing over single-digit dollars either way, and Postmark’s deliverability reputation is worth more than the difference.
Send on both channels with one credential
Email first. The documented body is to, from, subject and html:
curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "oncall@example.com",
"from": "alerts@yourdomain.com",
"subject": "Payment webhook backlog above threshold",
"html": "<p>Queue depth 4,120 for 12 minutes. Runbook: https://example.com/runbooks/webhooks</p>"
}'
{
"ok": true,
"data": {
"message_id": "msg_DgOWYJSuArAxcSI9MCzYLSJp",
"from_used": "alerts@yourdomain.com",
"mode": "domain",
"accepted_recipients": ["oncall@example.com"],
"suppressed_recipients": []
}
}
Watch suppressed_recipients. An address that bounced or complained earlier is dropped silently at send time, and if your only alerting path is email to one person who unsubscribed, you’ve built a pager that fails closed without telling you.
SMS takes to, body and from:
curl -sS -X POST "https://api.infrai.cc/v1/sms/send" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"to": "+14155550142",
"body": "SEV1 payments API 5xx above 10 percent. Ack in the console.",
"from": "InfraiAlerts"
}'
{
"ok": true,
"data": {
"message_id": "sms_9Qb2mXcT4kR7",
"state": "queued",
"vendor": "tencent_sms",
"segments": 1,
"cost_usd": 0.007475,
"created_at": 1784939403.45
}
}
segments and cost_usd come back on every send, so the cost of a bad template is visible immediately rather than at the end of the month. A 200-character alert is two segments and twice the price — trimming the runbook URL to a short link is a real saving at volume.
The routing rule, in code
Severity decides the channel; everything else is noise. This is Node 22 ESM, no dependencies:
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const BASE = "https://api.infrai.cc";
const headers = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
async function post(path, body) {
const res = await fetch(`${BASE}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
const payload = await res.json();
if (!res.ok || payload.ok === false) {
const err = payload.error ?? {};
throw new Error(`${path} failed: ${err.code ?? res.status} ${err.message ?? ""}`);
}
return payload.data;
}
export async function notify(event) {
const summary = `${event.service}: ${event.title}`;
if (event.severity === "sev1" && event.phone) {
return post("/v1/sms/send", {
to: event.phone,
body: summary.slice(0, 150),
from: "InfraiAlerts",
});
}
return post("/v1/email/send", {
to: event.email,
from: "alerts@yourdomain.com",
subject: summary,
html: `<p>${event.detail}</p><p><a href="${event.runbook}">Runbook</a></p>`,
});
}
One phone-call-shaped event per incident, everything else as email. If 2% of your alerts are sev1, blended cost per notification lands near $0.00026 — closer to the email rate than the SMS one, which is the whole point of routing rather than picking a channel.
Confirm it landed, then confirm what you spent
Both channels expose a free read path. GET /v1/email/list is concrete and needs no id, so it’s the fastest proof your integration is alive:
curl -sS "https://api.infrai.cc/v1/email/list?limit=5" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
curl -sS "https://api.infrai.cc/v1/account/usage" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Per-message detail needs the id from the send response — GET /v1/email/get/{id} returns state and per-recipient state, GET /v1/sms/status/{id} returns state plus failed_reason when a carrier rejects. Status reads, event timelines and suppression management are all free and rate-limited, so polling a delivery costs nothing but your own request budget.
The limitations, stated plainly
SMS on Infrai is currently western-region with tencent_sms as the ready vendor and Twilio listed as pending, so if your requirement is a specific US short code or a carrier relationship you already own, you’d be better off going direct to Twilio or Plivo and keeping the number under your account. Inbound SMS needs a configured vendor and returns SMS_INBOUND_NOT_SUPPORTED otherwise — two-way conversations aren’t what this surface is for. There’s no bundled notification-preference UI, no digest engine, no per-user quiet hours; that’s product logic you write.
And if alerts are genuinely the only thing you’re buying, a specialist is a defensible choice. Postmark’s separate transactional and broadcast streams protect your reset emails from your newsletter, and that’s a real feature.
What the single credential buys you is the second question. The queue that debounces duplicate alerts, the object store holding the incident snapshot, the error tracker that raised the event, the cron job that sweeps stale incidents, and the usage query that attributes all of it to a tenant are on the same account and the same invoice — no fifth vendor, no fifth key rotation, no reconciliation spreadsheet.