Resend vs Postmark developer experience, and a third option

How the two feel different once you get past hello-world: templating models, domain verification, event access, and where an aggregated API changes the call site.

Both get a Node service sending from a custom domain inside an afternoon, and both make you publish the same DKIM and return-path records first, so “easiest setup” isn’t really the axis that separates them. Resend optimises for the first hour — React Email, a clean SDK, a dashboard that reads like a developer tool. Postmark optimises for month nine, when someone asks why a password reset didn’t arrive and you need message history and a support engineer who speaks SMTP.

There’s a third shape worth knowing about, which is what Infrai does: keep the same REST call site and let the platform hold the vendor relationship. Infrai’s western email region runs on Resend today, so the deliverability characteristics are Resend’s — what changes is that templates, suppression, domain state and event history come back through one credential that also covers SMS, storage, cron and error tracking.

Where the two actually diverge

AxisResendPostmarkInfrai email API
TemplatingReact Email components, compiled in your appHandlebars-style templates stored server-sideServer-side templates with named variables and a preview call
First send from own domainDNS records, then sendDNS records, then sendDNS records via POST /v1/email/domain/verify, then send
Delivery historyDashboard plus APIDashboard search, long retention, its main selling pointGET /v1/email/list and GET /v1/email/event/list
StreamsSingle sending surfaceTransactional and broadcast streams kept apartSingle surface; separate streams aren’t modelled
Switching cost laterRewrite against a new SDKRewrite against a new SDKfrom and template_id stay put; the vendor moves underneath
Other services on the same keyEmail onlyEmail onlyEmail, SMS, storage, queues, cron, auth, error tracking

Postmark’s own comparison page is worth reading with the obvious caveat that they wrote it — it’s still the clearest statement of what they think matters, which is reputation isolation between transactional and marketing traffic.

Templates you can render before anyone receives them

This is the part that gets skipped in DX comparisons, and it’s the part that catches bugs. A server-side template with declared variables can tell you which ones you forgot, before the mail goes out.

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/email/template/create" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"name":"welcome-dx-compare","subject":"Welcome to {{product}}, {{first_name}}","html":"<p>Hi {{first_name}}, your {{product}} workspace is ready.</p>","variables":["first_name","product"]}'
{
  "ok": true,
  "data": {
    "template_id": "tmpl_qw407eYX1mZOSCIQMm9SFA7Y",
    "name": "welcome-dx-compare",
    "subject": "Welcome to {{product}}, {{first_name}}",
    "html": "<p>Hi {{first_name}}, your {{product}} workspace is ready.</p>",
    "created_at": "2026-07-25T15:54:00.544057Z",
    "variables": ["first_name", "product"]
  }
}

Now render it with a deliberately incomplete variable set:

curl -sS -X POST "https://api.infrai.cc/v1/email/template/preview/tmpl_qw407eYX1mZOSCIQMm9SFA7Y" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"vars":{"first_name":"Dana"}}'
{
  "ok": true,
  "data": {
    "rendered_subject": "Welcome to {{product}}, Dana",
    "rendered_html": "<p>Hi Dana, your {{product}} workspace is ready.</p>",
    "missing_vars": ["product"]
  }
}

missing_vars is the assertion you want in a unit test. In our testing the unresolved token is left in place rather than blanked, so a missed variable ships as literal {{product}} in the subject line of a real welcome email — a failure mode every templating system has, and one that a preview call turns into a red test instead of a support ticket.

React Email’s answer to the same problem is a component and a storybook. That’s genuinely nicer to build in, and if your team lives in JSX you’ll probably prefer it. The trade-off is that your email markup is compiled into your application, so changing a paragraph means a deploy rather than a PATCH /v1/email/template/update/{id}.

Sending the welcome message

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");

async function post(path, body) {
  const res = await fetch(API + path, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  const payload = await res.json();
  if (!res.ok || payload.ok === false) {
    const e = payload.error ?? { code: `HTTP_${res.status}`, message: res.statusText };
    throw new Error(`${path} -> ${e.code}: ${e.message}`);
  }
  return payload.data;
}

export async function sendWelcome({ email, firstName, product }) {
  const templateId = process.env.WELCOME_TEMPLATE_ID ?? "tmpl_qw407eYX1mZOSCIQMm9SFA7Y";
  const vars = { first_name: firstName, product };

  const preview = await post(`/v1/email/template/preview/${templateId}`, { vars });
  if (preview.missing_vars.length) {
    throw new Error(`welcome template missing: ${preview.missing_vars.join(", ")}`);
  }

  const sent = await post("/v1/email/send", {
    to: email,
    from: "welcome@mail.example.com",
    template_id: templateId,
    template_vars: vars,
  });

  if (sent.suppressed_recipients?.length) {
    console.warn(`suppressed, not delivered: ${sent.suppressed_recipients.join(", ")}`);
    return null;
  }
  return sent.message_id;
}

const id = await sendWelcome({ email: "user@example.com", firstName: "Dana", product: "Acme" });
console.log("queued as", id);

Two things about that flow are worth copying whichever vendor you land on. Preview before send, and treat suppression as a normal outcome rather than an error — an address that hard-bounced last month coming back through your signup form is routine, and the send returns it in suppressed_recipients instead of throwing.

The equivalent curl, if you’d rather see the wire format:

curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"to":"user@example.com","from":"welcome@mail.example.com","template_id":"tmpl_qw407eYX1mZOSCIQMm9SFA7Y","template_vars":{"first_name":"Dana","product":"Acme"}}'

Then check the state of a specific message — the path takes the message_id the send returned:

curl -sS "https://api.infrai.cc/v1/email/get/msg_DgOWYJSuArAxcSI9MCzYLSJp" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

US and EU, and what none of this solves

Region matters for two separate reasons and people conflate them. Data residency is a legal question about where message content sits at rest; delivery reputation is an operational question about which IPs the receiving MTA has seen before. Resend and Postmark both let you pick a region for the first; neither of them, nor Infrai, lets you outrun the second — a brand-new sending domain gets throttled everywhere until it has history.

Infrai reports the throttle explicitly rather than silently queueing, which we prefer, but it’s the same physics.

What it costs, and how to read today’s number

Infrai’s email send is $0.000115 per recipient, verified 2026-07-25, with domain, template, suppression and event calls free and rate-limited instead of metered. New accounts get a $2 credit, roughly 17,391 sends. Resend and Postmark both price in monthly tiers rather than per message, which is cheaper at low volume and less predictable when a batch job doubles your month.

Read the current rate rather than trusting a paragraph — prices drift downward as vendor discounts land:

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'))"

So which one

If deliverability support is what you’re buying — a person who reads your DMARC aggregates and tells you why Outlook is foldering you — stick with Postmark. That’s a service, not an API, and no aggregator resells it.

If your team wants email templates to be React components and you’re happy with one vendor relationship, Resend direct is the shortest path, and you skip a layer.

Infrai makes sense when email isn’t the only thing you’re integrating. The welcome mail is usually step three of five: a queue job to send it, storage for the rendered artefact, a cron entry for the day-3 nudge, error tracking when DNS breaks. Those arrive on the same key and the same invoice, and per-tenant cost is a query rather than a reconciliation across four dashboards. The honest limitation is vendor choice — POST /v1/email/send has no support for pinning a specific MTA per message, with Amazon SES and Tencent listed as pending rather than ready, so if you need a particular vendor’s IP pool you’re better off contracting with them directly.

References

Browse more email developer guides