Welcome email setup in Node: shared sender first, custom domain second

The order of operations for transactional welcome mail — send on day one, read the SPF and DKIM records the API returns, and know what a custom sender domain costs.

Setting up welcome mail has a natural order, and it isn’t the one most guides teach. You can put a real message in a real inbox from Node in about five minutes using Infrai’s shared sender, no DNS involved; registering your own domain — with SPF, DKIM and DMARC — is a separate step that you should take once the product has users worth branding for. Doing it in that order means your signup hook is already tested when the DNS propagates.

The catch is that a custom sender domain isn’t free on Infrai, and it’s better to know that now than after you’ve written the DNS records into a runbook. Details below.

Day one: a send with no sender configuration

Leave from out. The API stamps a shared address, and the response tells you exactly which one:

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
        "to": "new.user@example.com",
        "subject": "Welcome to Acme — here is what to do first",
        "html": "<h1>You are in</h1><p>Three things worth doing today: <a href=\"https://app.acme.dev/import\">import your data</a>, invite a teammate, and connect a repo.</p>"
      }'
{
  "ok": true,
  "data": {
    "message_id": "msg_2ZhTtleGakhMuXd68qzTrugF",
    "mode": "default_vendor",
    "from_used": "noreply+a1f9@send.infrai.cc",
    "accepted_recipients": ["new.user@example.com"],
    "suppressed_recipients": []
  }
}

from_used is the value the recipient will see. That address authenticates properly out of the box, which is why a first send behaves well before you’ve touched a nameserver — the trade-off is that it carries no brand at all.

The signup hook

Welcome mail fires once per account, on a state change you own. Keep it out of the request path: if the mail API is slow, the signup response shouldn’t be.

// welcome-mailer.mjs — Node 22
const API = "https://api.infrai.cc/v1/email/send";
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("set INFRAI_API_KEY before starting the worker");

const body = (account) => ({
  to: account.email,
  subject: `Welcome to Acme, ${account.firstName}`,
  html: `<h1>Hi ${account.firstName}</h1>
         <p>Your workspace <strong>${account.workspace}</strong> is ready.</p>
         <p><a href="https://app.acme.dev/onboarding?w=${account.workspaceId}">Finish setup</a></p>`,
});

export async function sendWelcome(account) {
  const res = await fetch(API, {
    method: "POST",
    headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
    body: JSON.stringify(body(account)),
  });

  const json = await res.json().catch(() => ({}));
  if (res.status === 429 || res.status >= 500) {
    const err = new Error(`transient ${res.status}`);
    err.retryable = true;
    throw err;
  }
  if (!res.ok || json.ok !== true) {
    throw new Error(`${json?.error?.code ?? res.status}: ${json?.error?.message ?? "send failed"}`);
  }
  if (json.data.suppressed_recipients.length) {
    console.warn("suppressed, not delivered:", json.data.suppressed_recipients);
    return null;
  }
  return json.data.message_id;
}

if (process.argv[2]) {
  const id = await sendWelcome({
    email: process.argv[2],
    firstName: "Sam",
    workspace: "sam-dev",
    workspaceId: "ws_8812",
  });
  console.log("queued as", id);
}

Two behaviours to notice. A 429 or a 5xx is retryable and belongs on a queue with backoff; a 4xx is your payload and retrying it just burns quota. And a suppressed recipient returns success — the address is on the account suppression list from an earlier bounce or complaint, so the send is accepted and nothing is delivered.

Step two: your own domain, and what it costs

POST /v1/email/domain/verify registers a sending domain and hands back the records to publish. On a standard account it answers with HTTP 402:

curl -sS -X POST "https://api.infrai.cc/v1/email/domain/verify" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"domain":"mail.acme.dev"}'
{
  "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
  }
}

That’s the honest shape of it: shared sending is included, branded sending is a paid upgrade. If a branded From line is non-negotiable on day one and you don’t want a plan, Resend and Mailgun both let you verify a domain on a free tier, and that is a fair reason to pick one of them for this specific job.

Once the account can hold domains, GET /v1/email/domain/list shows what’s registered and the exact record set:

curl -sS "https://api.infrai.cc/v1/email/domain/list" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "records": [
      {
        "domain": "mail.acme.dev",
        "status": "verified",
        "dns_records": [
          { "type": "TXT", "name": "mail.acme.dev", "value": "v=spf1 include:_spf.infrai.cc ~all", "purpose": "spf", "ttl_recommended": 3600 },
          { "type": "TXT", "name": "cf._domainkey.mail.acme.dev", "value": "v=DKIM1;k=rsa;p=MIIBIj...AB", "purpose": "dkim", "ttl_recommended": 3600 },
          { "type": "TXT", "name": "_dmarc.mail.acme.dev", "value": "v=DMARC1;p=none;rua=mailto:dmarc@infrai.cc", "purpose": "dmarc", "ttl_recommended": 3600 }
        ],
        "checks": { "spf_dns": "verified", "dkim_dns": "verified", "dmarc_dns": "verified", "mail_loopback": "verified" },
        "warm_up_state": "in_progress",
        "daily_limit_current": 50000,
        "daily_limit_target": 500000
      }
    ]
  }
}

Publish all three, wait for propagation — 3600 seconds of TTL means you may wait an hour — then call verify again until status flips. Mailgun’s authentication write-up is a good companion read on why DKIM alignment, not merely a passing SPF, is what receivers grade you on.

Warm-up is a real limit

daily_limit_current versus daily_limit_target is the part that surprises teams migrating a list. A new domain starts throttled and earns headroom by sending clean mail; sending 50,000 welcome messages on the first afternoon is how a fresh domain gets filtered. For an ordinary signup curve you’ll never touch the ceiling, which is exactly why welcome mail is the ideal traffic to warm a domain with.

Confirm the first one landed

curl -sS "https://api.infrai.cc/v1/email/list" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "items": [
      { "message_id": "msg_2ZhTtleGakhMuXd68qzTrugF", "state": "sent", "channel": "email", "to": "new.user@example.com", "vendor": "resend", "created_at": 1785025802.37 }
    ]
  }
}

state walks queuedsentdelivered or bounced. There’s no callback — delivery status is polled, which is a limitation worth designing around if you want an in-app “we couldn’t reach your address” banner.

The cost of the whole path

Sending is metered per message: $0.000115 per email, verified 2026-07-26 and flagged approximate because the vendor mix underneath moves. Domain registration, listing, message lookup and event history are free and rate-limited. New accounts hold $2 of credit, which is about 17,000 welcome messages before you pay anything. Prices here drift downward and promotions run, so read it live rather than trusting a paragraph:

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

Which sender to be on

SetupWhat it costsGood forWeak spot
Infrai shared senderIncluded; per-message rate onlyDay one, internal tools, stagingNo brand in the From line
Infrai custom domainPro planProducts where the sender is the brandPaid upgrade, plus DNS access
Specialist free tier (Resend, Mailgun)Free at low volumeBranded sending with zero budgetA second account, key and invoice
Your own SMTP relayA server and your weekendsFull control, odd compliance rulesReputation and blocklists are yours

The reason to run welcome mail on Infrai isn’t the per-message rate — it’s that the next three things this flow needs are on the same key. The delayed day-3 nudge is a cron entry, the retry queue is a queue, and per-tenant send cost is a usage query rather than a spreadsheet joining four vendor invoices. If email is genuinely the only external call your app will make, a specialist is the simpler purchase and you should make it.

References

Browse more email developer guides