Cheap deliverability for a startup: pick the sending subdomain first

Verify a subdomain rather than your apex, keep transactional and marketing reputations apart, and run DKIM and suppression upkeep with free API calls.

The deliverability decision that saves a startup the most costs nothing at all: send from mail.yourdomain.com, not from yourdomain.com. A subdomain gets its own reputation at receivers, so a bad month for one class of mail doesn’t drag your invoices and password resets down with it. On Infrai the whole domain surface — verification, DKIM rotation, suppression, status reads — sits on the same key you send with, so there’s no second dashboard to wire up; the reads are free and rate-limited on top, which means the only thing this decision costs you is fifteen minutes in your DNS zone.

Getting it wrong is expensive later, though, and it’s awkward to undo. A domain that has been sending for a year carries history you can’t transfer, so the choice you make on day one is the one you live with.

Four ways to pick a From domain

From domainReputation lives withDNS workWhere it hurts
Vendor’s shared domain (you@vendor-mail.net)The vendor’s pool, shared with strangersNoneLooks untrustworthy, no DMARC alignment with your brand
Your apex (billing@example.com)Your whole company, including the marketing toolSPF/DKIM/DMARC on the apexOne complaint-heavy campaign hurts password resets
One transactional subdomain (mail.example.com)That subdomain onlyRecords on the subdomainYou still need a second one before you send campaigns
Split subdomains (mail. + news.)Each class independentlyTwo verifications, two key setsSlightly more upkeep; suppression may still be shared

Row three is right for almost every startup under a few hundred thousand messages a month. Row four is where you go the day marketing asks for a broadcast tool.

Why the apex is the expensive option

DMARC policy discovery explains it. A receiver evaluating mail from mail.example.com looks for _dmarc.mail.example.com first, and if there’s no record there it walks up to the organisational domain and applies that policy — the sp tag exists precisely so you can set a stricter rule for subdomains than for the parent (RFC 7489 covers the lookup order). Publishing a record on the subdomain gives that traffic class its own policy, its own aggregate reports, and its own answer to the question “who’s failing alignment”.

Reputation works the same way. Receivers score the domain in the From header, and a subdomain that only ever sends receipts builds a narrow, clean profile.

Register it and take the records the API hands back. One thing to settle before you script this: custom sending domains are a Pro-plan capability, and the boundary is declared rather than discovered by accident — GET /v1/discovery reports minimum_tier: "pro" for the email.domain.verify capability, so a standard account gets HTTP 402 and a clear reason instead of a half-configured domain.

export INFRAI_API_KEY="your_infrai_api_key"

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.example.com"}'
{
  "ok": true,
  "data": {
    "domain": "mail.example.com",
    "domain_id": "dom_Kx8Nq3ZbVe6TfWm2",
    "status": "pending_dns",
    "warm_up_state": "not_started",
    "daily_limit_current": 500,
    "daily_limit_target": 50000,
    "dns_records": [
      { "purpose": "spf", "type": "TXT", "name": "mail.example.com", "value": "v=spf1 include:spf.example-vendor.net ~all", "ttl_recommended": 3600 },
      { "purpose": "dkim", "type": "TXT", "name": "cf._domainkey.mail.example.com", "value": "v=DKIM1;k=rsa;p=MIIBIjANBgkq...", "ttl_recommended": 3600 },
      { "purpose": "dmarc", "type": "TXT", "name": "_dmarc.mail.example.com", "value": "v=DMARC1;p=none;rua=mailto:dmarc@example.com", "ttl_recommended": 3600 }
    ]
  }
}

Note daily_limit_current: 500 a day to start, climbing toward the target as clean volume accumulates. That ramp is per verified domain, so a second subdomain starts its own climb from the bottom — which is an argument for verifying the marketing subdomain early, before anyone actually needs it.

Two subdomains, side by side

curl -sS "https://api.infrai.cc/v1/email/domain/list" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "records": [
      { "domain": "mail.example.com", "status": "verified", "warm_up_state": "warming", "created_at": "2026-05-02T11:20:44Z" },
      { "domain": "news.example.com", "status": "pending_dns", "warm_up_state": "not_started", "created_at": "2026-07-19T08:03:11Z" }
    ],
    "count": 2
  }
}

Two rows, two independent reputations, one credential. The per-domain detail read is where the numbers that matter live — bounce rate, complaint rate, how much of today’s cap you’ve used.

The upkeep script

Deliverability upkeep dies because it’s nobody’s ticket. Put it on a schedule instead, and keep it small enough that nobody argues about maintaining it.

#!/usr/bin/env python3
"""Weekly sending-domain check. Exits non-zero when something needs a human."""
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"}
BOUNCE_LIMIT = 0.02
COMPLAINT_LIMIT = 0.0005


def get(path):
    res = requests.get(API + path, headers=HEADERS, timeout=15)
    payload = res.json()
    if not res.ok or payload.get("ok") is False:
        err = payload.get("error", {"code": f"HTTP_{res.status_code}"})
        raise RuntimeError(f"{path} -> {err.get('code')}: {err.get('message', '')}")
    return payload["data"]


def audit():
    problems = []
    for row in get("/v1/email/domain/list")["records"]:
        name = row["domain"]
        if row["status"] != "verified":
            problems.append(f"{name}: still {row['status']} — publish the DNS records")
            continue
        detail = get(f"/v1/email/domain/get/{name}")
        rep = detail["reputation"]
        if rep["bounce_rate_30d"] >= BOUNCE_LIMIT:
            problems.append(f"{name}: bounce rate {rep['bounce_rate_30d']:.2%}")
        if rep["complaint_rate_30d"] >= COMPLAINT_LIMIT:
            problems.append(f"{name}: complaint rate {rep['complaint_rate_30d']:.3%}")
        cap = rep["current_daily_cap"] or 1
        if rep["used_today"] / cap > 0.8:
            problems.append(f"{name}: {rep['used_today']}/{cap} of today's cap used")
        for check, state in (detail["verification"].get("checks") or {}).items():
            if state != "verified":
                problems.append(f"{name}: DNS check {check} = {state}")
    return problems


found = audit()
for line in found:
    print(line, file=sys.stderr)
print(f"{len(found)} problem(s) across sending domains")
sys.exit(1 if found else 0)

Run it weekly. Every call it makes is free, so the only budget it consumes is rate limit.

DKIM rotation belongs on a longer cycle — annually is a common security-policy answer — and POST /v1/email/domain/rotate_dkim/{domain} replaces the key under the same cf._domainkey selector rather than publishing a second one. The catch is that in-place replacement leaves a propagation window where receivers still caching the old TXT value can fail the signature, so rotate at a quiet hour and re-check status before you resume normal volume.

Sending from the domain you just verified

curl -sS -X POST "https://api.infrai.cc/v1/email/send" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"to":"customer@example.com","from":"receipts@mail.example.com","subject":"Payment received","html":"<p>We received $29.00. Your plan renews on 25 August.</p>"}'

The reply echoes from_used, which is the field to assert on in a test — a mismatch means the domain isn’t verified and you’ve silently fallen back to a shared sender. An unverified sender domain surfaces as EMAIL_NOT_CONFIGURED.

What any of this costs

Verification, listing, status reads, rotation and suppression are all free — rate-limited rather than metered, and they don’t consume the new-account trial. Only the send is billable: $0.00046 per recipient on POST /v1/email/send, which is $0.46 per 1,000, verified 2026-07-27. Fanning one message out through POST /v1/email/batch/send is metered on its own line at its own rate, so price that route separately rather than assuming the two agree. Discovery also publishes a new_account_trial_uses count per billable route, which is the honest way to ask how far the signup credit goes: it’s recomputed against today’s rate instead of frozen into a sentence somebody wrote last quarter. Rates in this market move down rather than up as vendor discounts land, so pull today’s reading rather than quoting this paragraph:

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

Limits, and the two places we’d send you elsewhere

Worth flagging one thing that undercuts the split-subdomain plan: suppression entries here read back with scope: "account", so an address that unsubscribes from your newsletter is blocked for your receipts too. Splitting subdomains splits reputation at the receiver, not the block list on your side — if those must be independent, run marketing on a separate account or a separate vendor entirely.

One more, and it’s about hardware rather than plans: nothing here gives you a dedicated IP. Buy Amazon SES if your volume is high enough that a dedicated IP and its own warm-up curve are worth operating yourself. Buy Postmark if you want separate message streams for transactional and broadcast mail inside a single account — a cleaner model than two subdomains, once you can live with the pricing.

For a small team the argument for keeping it here is what sits next door on the same key: the receipt PDF in object storage, this weekly audit running as a platform cron entry, the failure it throws captured by error tracking, and GET /v1/account/usage answering what a tenant’s sending actually cost. One bill, one usage view, and no second account to open the week any of those becomes urgent.

References

Browse more email developer guides