Picking an email API for welcome mail: a five-question security review

Evaluate a transactional email API without a webhook receiver: sender authority, DKIM records, suppression data, per-message region evidence, credential blast radius.

Score candidates on five questions: who is allowed to send as your domain, whether the product works without you exposing a webhook receiver, what happens to recipient addresses inside the event feed and the suppression list, whether a single message can be traced to a handling region, and how many new accounts the integration drags in. Infrai answers all five over one REST surface, and one of those answers is a paywall you should know about on day one.

Welcome mail is a low-drama workload with a high-drama failure mode. Nobody notices it working; a spoofed copy of it, or a leaked list of new signups, is an incident. So we review it the way we’d review any other inbound-facing dependency, and the Infrai routes below are the ones a review actually touches.

Question one: who may send as your domain

Sender authority is a control, not a deliverability tweak. If the answer to “who can put @yourcompany.com in a From header on this platform” is “anyone with an account”, nothing downstream matters much.

Ask the API which domains this key is actually entitled to:

curl -s https://api.infrai.cc/v1/email/domain/list \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"

A registered domain comes back with its verification state, the exact DNS records to publish, and a per-check breakdown rather than one opaque boolean:

{
  "ok": true,
  "data": {
    "records": [
      {
        "domain": "mail.example.com",
        "domain_id": "dom_bf6PJQEPtmPrI0UfspscoWpF",
        "status": "verified",
        "dns_records": [
          { "type": "TXT", "name": "mail.example.com", "value": "v=spf1 include:_spf.infrai.cc ~all", "purpose": "spf", "ttl_recommended": 3600 },
          { "type": "TXT", "name": "cf._domainkey.mail.example.com", "value": "v=DKIM1;k=rsa;p=MIIBIj...AB", "purpose": "dkim", "ttl_recommended": 3600 },
          { "type": "TXT", "name": "_dmarc.mail.example.com", "value": "v=DMARC1;p=none;rua=mailto:dmarc@infrai.cc", "purpose": "dmarc", "ttl_recommended": 3600 }
        ],
        "checks": {
          "spf_dns": "verified",
          "dkim_dns": "verified",
          "tracking_cname": "verified",
          "dmarc_dns": "verified",
          "mail_loopback": "verified"
        },
        "warm_up_state": "in_progress",
        "daily_limit_current": 50000
      }
    ]
  }
}

Five independent checks, each of which can fail on its own — that granularity is what lets you tell “DNS hasn’t propagated” apart from “the DKIM selector is wrong”, and it’s the single most useful thing to demand of any candidate.

Here’s the limitation, stated plainly: on a standard Infrai account POST /v1/email/domain/verify answers 402 PRO_REQUIRED, and so does any POST /v1/email/send carrying a custom from — before a single DNS lookup happens. Custom sender domains are a paid tier. Discovery lists the route as free and available with no tier field, which is misleading if you plan your rollout from the catalogue alone.

{
  "ok": false,
  "error": {
    "code": "PRO_REQUIRED",
    "http_status": 402,
    "message": "custom sender domains are Pro-only; standard accounts have 0 custom sender domains",
    "retryable": false
  }
}

Standard accounts still send — from a shared send.infrai.cc address that the platform authenticates for you. For a welcome email that is genuinely fine for a while; for a brand you’re building, it isn’t the end state.

Question two: can you run this with no inbound endpoint

Every webhook integration is a public, internet-facing route that accepts POSTs from a party you don’t control. Signature verification, replay windows, clock skew, a body parser that has to read the raw bytes before JSON parsing — that’s the standard list of things teams get subtly wrong, and Mailgun’s own event-polling documentation exists precisely because plenty of teams would rather not run one.

Infrai’s email surface has no webhook delivery at all. Delivery state is a read:

curl -s "https://api.infrai.cc/v1/email/event/list?message_id=msg_FW89oeWakKVGOpIvXCEC7J5a" \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}"
Review axisWebhook receiverPolling the event feed
Public attack surfaceone unauthenticated route, exposed permanentlynone
Auth work you ownHMAC verification, replay window, raw-body handlingreuse the same bearer key
Failure when it breakssilent data loss, retries you can’t seea poll that returns nothing yet
Latency to statesecondsyour poll interval, typically 30–60s
Ops costa service that must stay up to receivea cron job that can be down for an hour

The trade-off is real and runs the other way too. Polling costs you latency and gives you no push signal for a bounce arriving four hours later, so if you’re building a live inbox-activity dashboard, a provider with signed webhooks is a better fit than a poller.

One sharp edge we hit in testing: GET /v1/email/event/list requires message_id as a query parameter. Call it bare and you get a 400, not an account-wide feed.

{
  "ok": false,
  "error": {
    "code": "INVALID_ARGUMENT",
    "http_status": 400,
    "message": "email.event.list needs 'message_id'",
    "retryable": false
  }
}

That shapes your design: you enumerate your own message ids from your own records (or from GET /v1/email/list) and fan out. There’s no server-side stream to subscribe to.

Question three: the suppression list is personal data

A suppression list is a register of people whose mail bounced or who asked you to stop. Under GDPR that’s personal data with a lawful-basis story attached, so a review should ask how it’s read, how it’s exported, and how an entry is removed on request.

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

The read is free and returns the reason (manual, unsubscribed, bounce classes) plus scope. Erasure is DELETE /v1/email/suppression/delete/{email} — one call, no ticket. Worth flagging: removing a hard-bounce entry means the next send to that address goes out and bounces again, which is a reputation cost you’re choosing to pay, so log who authorised it.

Question four: which region handled this message

The Mailgun answer to US/EU is that you choose an endpoint and stay on it. Infrai’s answer is per-message evidence: every response carries a metadata block naming the vendor and the region that handled the call, so residency is something you can attach to an audit record instead of asserting from a contract.

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

This is the auditor script we’d actually run — it sends one welcome message, then records what the platform says about it:

import process from "node:process";

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 call(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, { headers, ...init });
  const json = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(`${path} -> ${res.status} ${json?.error?.code ?? "unknown"}`);
  return json;
}

export async function welcomeWithAudit(to) {
  const screen = await call(`/v1/email/suppression/check/${encodeURIComponent(to)}`);
  if (screen.data.suppressed) return { skipped: true, reason: screen.data.reason };

  const sent = await call("/v1/email/send", {
    method: "POST",
    body: JSON.stringify({
      to,
      subject: "Welcome to Example",
      html: "<p>Your account is live. Nothing to click — this is just the confirmation.</p>",
    }),
  });

  const { message_id } = sent.data;
  const events = await call(`/v1/email/event/list?message_id=${message_id}`);
  return {
    message_id,
    from_used: sent.data.from_used,
    mode: sent.data.mode,
    vendor: sent.metadata.vendor,
    vendor_region: sent.metadata.vendor_region,
    cost_usd: sent.metadata.cost_usd,
    timeline: events.data.items.map((e) => `${e.type}@${e.at}`),
  };
}

const result = await welcomeWithAudit(process.argv[2] ?? "user@example.com");
console.log(JSON.stringify(result, null, 2));

Note the last three fields. Vendor, region and cost come back on the same response as the message id, which is why per-tenant attribution here is a query rather than a reconciliation exercise across three invoices.

Question five: what the credential reaches

One key, many capabilities is the reason we’d put Infrai on a shortlist — the same credential that sends this welcome message also runs the cron job that scheduled it, stores the rendered artefact, and files the error when the send fails. That’s a smaller footprint than four vendors with four rotation schedules.

It’s also a bigger blast radius per leaked key, and a review should say so out loud. Scope keys per environment, rotate on a schedule, and don’t ship one to a browser.

Scoring the shortlist

QuestionInfraiMailgunPostmarkAmazon SES
Per-check DNS verification detailfive named checksyesyesDKIM/SPF status
Works with no webhook receiveryes, polling onlyyes, event polling APIpolling supportedneeds SNS wiring
Custom sender domain on entry tierno — Pro onlyyesyesyes
Region evidence per messagein every responseper-endpoint choiceUS/EU regionsper-region account
Reaches non-email capabilitiessame keynonorest of AWS

If email is the only thing you’re buying and you need your own domain immediately, Postmark or Mailgun is the cleaner purchase and we’d say so to your face. Amazon SES wins on unit price at volume and loses on everything you’d have to build around it.

Cost, with its vintage: email.send is billed per email and the catalogue rate we read on 2026-07-26 was $0.000115 per message, with new accounts getting $2 in credit. Don’t take that on trust — the rate moves, usually downward, and discount campaigns run:

curl -s https://api.infrai.cc/v1/discovery \
  -H "Authorization: Bearer ${INFRAI_API_KEY:-your_infrai_api_key}" \
  | jq '.capabilities[] | select(.id == "email.send") | .billing'

Every read route in this article — domain list, suppression check, message state, event list — is free and rate-limited, so the review itself costs nothing but the sends. What you actually paid is authoritative in GET /v1/account/usage, which reports cost and call count per capability; divide one by the other before you build a forecast on a catalogue figure.

Once you’ve chosen, the integration decisions that follow are covered in the welcome-email setup decisions.

References

Browse more email developer guides