Welcome-email deliverability without SMTP: verify, then read the ramp

An API-first setup for welcome mail: the three sender states, the DNS checks map, and the warm-up cap you can query instead of guessing at.

Welcome mail is the first message a paying user ever gets from you, and it lands in the same folder as everyone else’s marketing. Getting it into the inbox is mostly two things: authenticate the domain properly, and don’t exceed the volume the receiving side is willing to accept from a new sender. Infrai exposes both as API reads — a DNS checks map and a daily cap you can query — which means the setup can be a script rather than a checklist someone half-remembers.

An SMTP relay can do this too. It just gives you a socket and a status line where an HTTPS call gives you a message id, and that difference compounds over a year of support tickets.

SMTP relay or HTTP API

SMTP relayHTTP API
CredentialUsername and password on a long-lived connectionBearer key in a header, rotatable
Per-message handleA queue id, if the relay bothersmessage_id returned synchronously
Failure surfaceConnection resets, TLS negotiation, port 587 blockedHTTP status plus a typed error code
Retry semanticsYour MTA decides, often invisiblyYours, explicitly, in application code
Local developmentNeeds an open port and a relay accountA curl command
Deliverability dataParse bounce mailA queryable event feed

Neither column is wrong. If you have a legacy application that only speaks SMTP, or a Postfix install already doing spooling you trust, running that against Amazon SES’s relay is a perfectly reasonable answer and cheaper than a rewrite. For a new Node service, the API side wins on debuggability alone.

The three states your sender can be in

Every welcome-email setup passes through the same sequence, and knowing which state you’re in tells you what’s actually possible.

Unregistered. The account has no domain of its own, sends go out from the shared send.infrai.cc address, and the response reports the exact sender in from_used. Mail works. Your brand doesn’t appear in the From: line, and the reputation belongs to the platform.

Pending DNS. You’ve registered a domain and it’s waiting on records — this is the state most setup guides gloss over, and where teams sit for two days because one TXT was pasted with a trailing dot.

Verified and warming. Authentication passes, and the sending cap starts below your target and climbs.

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

The list returns each domain’s status, its dns_records array, a checks object and the current warm-up numbers. That’s enough to build the gate below without a dashboard.

One honest note up front: registering a custom sender domain is a Pro-plan feature. On a standard key POST /v1/email/domain/verify answers 402 PRO_REQUIRED, so if your plan is “ship on the shared sender forever”, the deliverability ceiling in this article isn’t available to you.

Read the checks map, don’t eyeball the DNS

Four records matter, and the API tells you which one is failing instead of making you run dig five times:

{
  "status": "verified",
  "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,
  "daily_limit_target": 500000
}

mail_loopback is the one people miss. SPF, DKIM and DMARC can all resolve correctly while the domain still can’t complete a round trip, and the loopback check is what catches that before your users do.

DMARC starts at p=none deliberately. Move it to quarantine after a couple of weeks of clean reports, not on day one — a strict policy over a misconfigured SPF record will bin your own welcome mail.

A deploy-time gate in Node

Don’t let a release flip the sender over to a custom domain that hasn’t finished verifying. Make it a startup check:

// sender-preflight.mjs — Node 22 ESM. Fails the deploy if the sender isn't ready.
const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
const DOMAIN = process.env.SENDER_DOMAIN ?? "mail.example.net";
if (!KEY) throw new Error("INFRAI_API_KEY is required (your_infrai_api_key)");

const res = await fetch(`${API}/v1/email/domain/list`, {
  headers: { authorization: `Bearer ${KEY}` },
});
const payload = await res.json();
if (!res.ok || payload.ok === false) {
  throw new Error(`domain list failed: ${payload?.error?.code ?? res.status}`);
}

const record = payload.data.records.find((d) => d.domain === DOMAIN);
if (!record) {
  console.warn(`${DOMAIN} not registered — falling back to the shared sender`);
  process.env.USE_CUSTOM_SENDER = "false";
} else {
  const failing = Object.entries(record.checks ?? {})
    .filter(([, state]) => state !== "verified")
    .map(([name]) => name);

  if (record.status !== "verified" || failing.length) {
    throw new Error(`${DOMAIN} not ready: status=${record.status} failing=${failing.join(",") || "none"}`);
  }

  const headroom = record.daily_limit_current;
  console.log(`${DOMAIN} verified; today's cap is ${headroom} messages (target ${record.daily_limit_target})`);
  if (headroom < Number(process.env.EXPECTED_DAILY_SENDS ?? 0)) {
    throw new Error(`cap ${headroom} is below the expected daily volume`);
  }
  process.env.USE_CUSTOM_SENDER = "true";
}

Wire that into your container’s start command and a half-verified domain becomes a failed deploy instead of a week of mail silently going to spam.

The ramp is a number, not a feeling

Warm-up advice on most vendor blogs is folklore: “start slow, increase gradually”. The reputation block replaces that with thresholds you can assert on. A new domain sits in the warming_up tier with a current cap well below its target, and promotion to established has explicit requirements — a 30-day bounce rate under 2%, a complaint rate under 0.05%, and 30 days in tier.

That reframes the welcome email as a hygiene problem rather than a volume problem. Nothing raises a bounce rate faster than mailing a signup list you never validated, so check the suppression list before a bulk import runs:

curl -sS https://api.infrai.cc/v1/email/suppression/check/dana@example.net \
  -H "Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY=your_infrai_api_key}"

And send the welcome itself with a plain-text alternative in body, because HTML-only mail scores worse with several major providers:

curl -sS -X POST https://api.infrai.cc/v1/email/send \
  -H "Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY=your_infrai_api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "dana@example.net",
    "subject": "Welcome to Ledgerly",
    "html": "<p>Hi Dana, your workspace is ready. <a href=\"https://app.example.net/connect\">Connect a data source</a>.</p>",
    "body": "Hi Dana, your workspace is ready. Connect a data source: https://app.example.net/connect",
    "reply_to": "support@example.net"
  }'

Then confirm it, per message, without standing up a webhook receiver:

curl -sS https://api.infrai.cc/v1/email/get/msg_FjDRVM4y1dx7xcElLubSlJMF \
  -H "Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY=your_infrai_api_key}"

That read returns state, the recipient, the vendor that carried it and the creation timestamp. The catch is that it’s a poll: there are no email webhooks in this namespace, so a real-time dashboard means a worker on a timer. At welcome-email volumes that’s a fine trade-off; at millions of messages a day it isn’t, and a provider with push notifications is the better fit.

Cost and where this argument actually rests

Sends run $0.000115 each, read on 2026-07-26. Domain reads, suppression reads, template calls and event lookups are free and rate-limited, so the setup work above costs nothing at all — you pay only for delivered attempts.

curl -sS https://api.infrai.cc/v1/discovery \
  -H "Authorization: Bearer ${INFRAI_API_KEY:?set INFRAI_API_KEY=your_infrai_api_key}" \
  | python3 -c "import sys,json;d=json.load(sys.stdin);print([(c['id'],(c.get('billing') or {}).get('price_usd')) for c in d['capabilities'] if c['id'] in ('email.send','email.domain.verify')])"

Per-message pricing across this market has fallen for a decade and shows no sign of stopping, so the live number is likely below the one printed here — which is precisely why choosing a provider on rate alone is a bad idea. Mailgun and SendGrid will both quote you something comparable, and both are stronger than Infrai on marketing-side tooling.

The reason to run welcome mail here is that the signup flow around it — the queue holding the job, the storage bucket with the user’s imported file, the error tracker that catches a failed render — is reachable with the same key and lands on the same invoice, with per-tenant attribution as a single usage query. If email is genuinely the only thing you’ll ever buy, pick a specialist and be happy.

References

Browse more email developer guides