Launch day on a brand-new sending domain: read the cap, don't blast

A fresh domain has no reputation and a throttled daily cap. What a 40,000-message blast really does, and a ramp planner that reads the live numbers first.

Blasting 40,000 messages from a domain that has never sent one is the single most reliable way to land in spam folders and stay there. Receiving providers grade an unknown domain by treating volume as risk, and Infrai layers its own throttle on top: a new sending domain carries a daily_limit_current well below its daily_limit_target, and a reputation tier that only advances on evidence. The good news is that all of those numbers are readable before you send anything.

So the plan isn’t “hope”; it’s arithmetic against a cap you can query.

What a blast actually costs you

Three things go wrong at once, and they compound. Your list is stale, so a cold blast produces a bounce spike, and bounce rate is the metric every receiver and every sending platform grades hardest. The tier ladder then stalls, because advancement has an explicit bounce threshold. And a domain that gets filtered on its first big day carries that history into every subsequent send — reputation is sticky in a way that daily caps are not.

There’s an error code for the end state of that story, EMAIL_REPUTATION_SUSPENDED, and you never want to meet it.

The numbers, before you send

GET /v1/email/domain/get/{domain} returns verification state and reputation in one response:

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS "https://api.infrai.cc/v1/email/domain/get/send.example.com" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "verification": {
      "domain": "send.example.com",
      "domain_id": "dom_bf6PJQEPtmPrI0UfspscoWpF",
      "status": "verified",
      "warm_up_state": "in_progress",
      "daily_limit_current": 50000,
      "daily_limit_target": 500000
    },
    "reputation": {
      "tier": "warming_up",
      "current_daily_cap": 50000,
      "used_today": 0,
      "bounce_rate_30d": 0.0,
      "complaint_rate_30d": 0.0,
      "days_in_current_tier": 0,
      "next_tier": "established",
      "next_tier_requirements": {
        "bounce_rate_30d_lt": 0.02,
        "complaint_rate_30d_lt": 0.0005,
        "days_in_current_tier_gte": 30
      },
      "throttle_risk": "low"
    }
  }
}

Read next_tier_requirements as the contract it is. Under 2% bounces, under 0.05% complaints, and 30 days of that behaviour moves the domain from warming_up to established. Those are not arbitrary — they’re roughly where Gmail’s own bulk sender guidance draws its complaint line too, which is why hitting them helps with far more than one platform’s throttle.

One trap we hit while testing on 2026-07-26: the same route against a domain that hasn’t finished DNS verification returns reputation: null and a warm_up_state of not_started. Your planner has to handle that rather than dereferencing straight into reputation.current_daily_cap.

{
  "ok": true,
  "data": {
    "verification": {
      "domain": "mail.example.com",
      "status": "pending_dns",
      "warm_up_state": "not_started",
      "daily_limit_current": 50000,
      "daily_limit_target": 500000
    },
    "reputation": null
  }
}

A ramp that earns the ceiling

The shape below is the standard warm-up curve, ordered by engagement rather than by database id. Send to the people most likely to open first — recent signups, active accounts — because early positive signal is what buys the later headroom.

DayShare of the listOrderingCheck before continuing
11,000most recently activebounce_rate_30d under 0.02
22,000active in 30 dayscomplaints still at zero
34,000active in 90 daysthrottle_risk still low
58,000active in 180 daysused_today vs current_daily_cap
716,000remaining engagedtier unchanged is fine; falling is not
10+the rest, or neverdormant addressesconsider not sending at all

Doubling daily is aggressive but survivable when the list is clean. If the first tranche bounces above 2%, don’t double — stop, and go fix the list.

That last row is not a joke. Addresses that haven’t opened anything in a year are the ones that bounce or complain, and mailing them on launch day is paying real money to damage an asset you just built.

The planner

This reads the live cap, subtracts what’s already gone out today, applies the ramp, and refuses to hand you a number that would exceed the ceiling.

// ramp-planner.mjs — Node 22, no dependencies.
import process from "node:process";

const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");

const DOMAIN = process.argv[2] ?? "mail.example.com";
const RAMP = [1000, 2000, 4000, 4000, 8000, 8000, 16000];
const MAX_BOUNCE = 0.02;
const MAX_COMPLAINT = 0.0005;

async function readDomain(domain) {
  const res = await fetch(`${API}/v1/email/domain/get/${encodeURIComponent(domain)}`, {
    headers: { authorization: `Bearer ${KEY}` },
  });
  const payload = await res.json().catch(() => ({}));
  if (res.status === 404) throw new Error(`${domain} is not registered on this account`);
  if (!res.ok || payload.ok === false) {
    throw new Error(payload.error?.code ?? `HTTP_${res.status}`);
  }
  return payload.data;
}

function plan(data, dayIndex) {
  const { verification, reputation } = data;
  if (verification.status !== "verified") {
    return { allowed: 0, reason: `domain is ${verification.status}; publish the DNS records first` };
  }
  if (!reputation) {
    return { allowed: 0, reason: "no reputation record yet — verification has not completed" };
  }
  if (reputation.bounce_rate_30d >= MAX_BOUNCE) {
    return { allowed: 0, reason: `bounce rate ${reputation.bounce_rate_30d} is at or above ${MAX_BOUNCE}` };
  }
  if (reputation.complaint_rate_30d >= MAX_COMPLAINT) {
    return { allowed: 0, reason: `complaint rate ${reputation.complaint_rate_30d} is too high to continue` };
  }
  const headroom = Math.max(0, reputation.current_daily_cap - reputation.used_today);
  const target = RAMP[Math.min(dayIndex, RAMP.length - 1)];
  return {
    allowed: Math.min(target, headroom),
    reason: target <= headroom ? "on plan" : `capped by remaining headroom (${headroom})`,
    tier: reputation.tier,
    throttleRisk: reputation.throttle_risk,
  };
}

const day = Number(process.argv[3] ?? 0);
const data = await readDomain(DOMAIN);
console.log(JSON.stringify(plan(data, day), null, 2));

Run it from the same job that dequeues the launch list, and treat allowed: 0 as a hard stop rather than a warning to ignore. A launch that takes nine days and lands is better than one that takes an afternoon and doesn’t.

Confirm which domains the account actually holds before you plan against one:

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

Hygiene does more than pacing

Run every address through GET /v1/email/suppression/check/{email} before it enters a tranche, since a previously bounced address will be dropped anyway and there’s no reason to spend a send on it. For the sending itself, POST /v1/email/batch/send exists for multi-recipient jobs and enforces a size ceiling — EMAIL_BATCH_TOO_LARGE is the error you’ll see if you overshoot it, and the request shape is worth confirming against the API reference before you build against it rather than guessing from a blog post.

The part that stops a standard account cold

None of the above is reachable without a Pro plan. POST /v1/email/domain/verify answers HTTP 402 PRO_REQUIRED on a standard key — “standard accounts have 0 custom sender domains” — and the same 402 fires on a send with a custom from before any DNS is inspected. So on a standard account you’re on the shared sender, whose warm-up isn’t yours to manage or to damage.

That’s a genuine trade-off. If launch-day branded volume is the requirement and you’d rather not upgrade, Amazon SES is the honest recommendation here: dedicated IPs with a managed warm-up plan, and a per-message price that’s hard to beat at tens of thousands of messages. Mailgun’s scheduled delivery and rate controls are the other reasonable pick if you want the pacing handled for you rather than in your own worker.

What the launch costs

Domain reads, suppression checks and message lookups are free and rate-limited, so the planner itself is free to run every minute. Sends are metered at $0.000115 per email, verified 2026-07-26 and flagged approximate, so a 40,000-message launch is roughly $4.60 of sending — the credible risk on launch day is reputational, not financial. New accounts hold $2 of free credit. Rates drift down as vendor discounts land, so read the live figure:

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.batch.send"):
        print(c["id"], c["billing"].get("price_usd"), c["billing"].get("unit"))'

The reason to run a launch here rather than on a point solution is what surrounds it: the tranche scheduler is a cron entry, the retry backlog is a queue, the planner’s failures land in error tracking, and the whole thing bills to one account. Same key, one usage view, no second onboarding on the week you can least afford it.

References

Browse more email developer guides