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 cheapest deliverability decision a startup makes 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, key rotation, status reads — is free and rate-limited, 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 domain | Reputation lives with | DNS work | Where it hurts |
|---|---|---|---|
Vendor’s shared domain (you@vendor-mail.net) | The vendor’s pool, shared with strangers | None | Looks untrustworthy, no DMARC alignment with your brand |
Your apex (billing@example.com) | Your whole company, including the marketing tool | SPF/DKIM/DMARC on the apex | One complaint-heavy campaign hurts password resets |
One transactional subdomain (mail.example.com) | That subdomain only | Records on the subdomain | You still need a second one before you send campaigns |
Split subdomains (mail. + news.) | Each class independently | Two verifications, two key sets | Slightly 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:
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.000115 per recipient, verified 2026-07-26, with the $2 signup credit covering roughly 17,391 messages. Prices in this market move down rather than up as vendor discounts land, so pull today’s number 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.
Two more. Custom sender domains need a paid plan, and POST /v1/email/domain/verify answers HTTP 402 PRO_REQUIRED on a standard account. And nothing here gives you a dedicated IP: Amazon SES sells dedicated IPs attached to configuration sets, and if your volume is high enough to warrant one, that’s the better buy. Postmark takes the opposite approach with separate message streams for transactional and broadcast mail on one account, which is a cleaner model than two subdomains if you can live with its pricing.
For a small team the argument for keeping it here is what sits next door: the receipt PDF in object storage, the weekly audit above on the platform’s cron, the alert in error tracking, one bill and one usage view instead of four vendor invoices to reconcile.