Postmark, Resend or Mailgun for an EU SaaS: residency, price shape, one key

How the three compare on processing region, sub-processors and billing shape for a small EU SaaS, plus a Python send-and-record loop on Infrai's email API.

Three questions decide this for a small EU-based SaaS, and the per-email rate is the last of them: where the message is processed and under whose sub-processor list, whether you’re buying a monthly plan or a meter, and how much bounce plumbing you have to operate yourself. Mailgun and Resend both let you pin sending to an EU region; Postmark doesn’t market one. Infrai sits in a different column — one credential, metered sends, and no regional pin today.

That last point is a real constraint, so it’s worth putting up front rather than burying it under a price table.

What GDPR asks of a transactional email provider

A welcome or password-reset email to your own signed-up user is usually processed under contract performance, not consent, so the debate isn’t about opt-in. It’s about the processor chain: you need a data processing agreement, a current sub-processor list, a transfer mechanism if the data leaves the EEA, a retention answer, and a way to satisfy an erasure request that reaches every copy — including the suppression list, which is personal data that most teams forget they hold.

ProviderRegion controlBilling shapeBounce/suppression handlingBest fit
PostmarkUS processing, published DPAMonthly tier by volumeManaged, strong reputation toolingDeliverability-critical mail, budget to match
ResendRegion choice including EUMonthly tier + included volumeManaged, webhook-firstEU-only processing on a modern API
MailgunDedicated EU endpointMonthly tier + metered overageManaged, mature bounce classificationHigh volume with an EU residency requirement
InfraiWestern routing, no per-request pinMetered per email, one billManaged suppression list, poll or webhookTeams that need email plus six other services

If a signed DPA naming an EU-resident sub-processor is a procurement blocker for you, that table has a clear answer and it isn’t us. Mailgun’s EU endpoint and Resend’s region selection are the direct routes to it, and you’d be better off taking one of them than trying to argue the point with a customer’s legal team.

Every response tells you who handled the message

What Infrai does give you is provenance in-band. Each call returns a metadata block naming the vendor that handled it and the region it ran in, so your audit trail doesn’t depend on a support ticket.

curl -s -X POST https://api.infrai.cc/v1/email/send \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "dana@example.com",
    "from": "hello@example.com",
    "subject": "Your Acme workspace is ready",
    "html": "<p>Hi Dana — sign in whenever you like.</p>"
  }'
{
  "ok": true,
  "data": {
    "message_id": "msg_DVq65o59QbDTEGzHQBn0Ysbx",
    "from_used": "hello@example.com",
    "mode": "live",
    "accepted_recipients": ["dana@example.com"],
    "suppressed_recipients": []
  },
  "metadata": {
    "request_id": "req_0833341457154568ae366afc",
    "vendor": "resend",
    "vendor_region": "western",
    "latency_ms": 71
  }
}

vendor_region is the field your compliance reviewer will ask about. Read it, store it, and if it ever needs to say something other than western for your workload, that’s the signal to move this particular capability to a provider with an EU endpoint.

Send and record, in Python 3

The pattern below is the one that survives an audit: send, immediately read the message back, and append a row that pins the message to a vendor, a region and a timestamp. It’s twelve lines of bookkeeping that turn “we think it went via an EU processor” into evidence.

#!/usr/bin/env python3
"""Send a transactional email and record its processing provenance."""
import csv
import os
import sys
import requests

API = "https://api.infrai.cc"
KEY = os.environ.get("INFRAI_API_KEY")
if not KEY:
    sys.exit("INFRAI_API_KEY is not set")
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}


def send(to_addr: str, subject: str, html: str) -> dict:
    res = requests.post(
        f"{API}/v1/email/send",
        headers=HEADERS,
        json={"to": to_addr, "from": "hello@example.com", "subject": subject, "html": html},
        timeout=20,
    )
    payload = res.json()
    if not payload.get("ok"):
        raise RuntimeError(payload.get("error", {}).get("code", f"HTTP_{res.status_code}"))
    return payload


def record(payload: dict, path: str = "email_provenance.csv") -> None:
    data, meta = payload["data"], payload.get("metadata", {})
    row = [
        data["message_id"],
        meta.get("vendor", "unknown"),
        meta.get("vendor_region", "unknown"),
        meta.get("timestamp", ""),
    ]
    with open(path, "a", newline="") as fh:
        csv.writer(fh).writerow(row)


if __name__ == "__main__":
    out = send("dana@example.com", "Your Acme workspace is ready", "<p>Hi Dana.</p>")
    record(out)
    print(out["data"]["message_id"])

Erasure requests reach further than your database

When a user asks to be forgotten, the row in your users table is the easy part. The suppression list holds their address too, and so does the message history. Check first, then decide — an address that hard-bounced or complained is on that list for a reason, and deleting the entry means the next send to it will be attempted again.

curl -s https://api.infrai.cc/v1/email/suppression/check/user@example.com \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"
{
  "ok": true,
  "data": {
    "email": "user@example.com",
    "reason": "manual",
    "added_at": "2026-07-04T17:02:22.803322Z",
    "scope": "account",
    "attempt_count_blocked": 0,
    "suppressed": true
  }
}

Removing it is one call, and it’s free like the rest of the management surface:

curl -s -X DELETE https://api.infrai.cc/v1/email/suppression/delete/user@example.com \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"

Retention of message bodies and event history is configurable rather than infinite; a rejected value comes back as EMAIL_INVALID_RETENTION, which is the error to read before you write a policy your provider can’t honour.

The money, and how to check it yourself

Sends are metered at $0.000115 per email — verified 2026-07-26, flagged approximate because it tracks the underlying vendor mix. Everything around the send is free: domain verification, DKIM rotation, template CRUD, suppression management, event history, message reads. A new account starts with $2 free, which is about 17,391 sends before you pay anything.

Rates in this category have drifted downward for years and discount campaigns run, so read today’s number rather than this paragraph:

curl -s https://api.infrai.cc/v1/discovery \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  | grep -o '"id":"email.send"[^}]*}[^}]*}' \
  | head -1

Structurally, a meter and a plan behave differently at the edges. Below a few thousand sends a month, a plan means paying for a bundle you don’t use; above your tier’s ceiling, a plan is usually the cheaper of the two. Small EU SaaS products tend to spend a long time in the first case.

The honest summary

Pick Mailgun or Resend if EU-region processing is a contractual requirement — that’s a limitation of Infrai’s email routing today, not a preference. Pick Postmark if inbox placement for critical mail justifies a premium and you want their support team on it. Pick Infrai when email is one of several things you need and you’d rather not run four vendor accounts to ship one onboarding flow: the same key covers storage, queues, cron, error tracking and inference, and per-tenant cost attribution is a single usage query instead of a reconciliation across invoices.

References

Browse more email developer guides