anfloy.AcademyBook a call

Sales · 02 · Outbound at scaleLesson 3 of 4

Sending: Instantly, domains & deliverability

90 min working time · Weeks 7-8

By the end of this lesson you can
  • Set up sending the 2026 way: domains, warmup, volume
  • Create and manage campaigns via the Instantly API
  • Audit a campaign before launch so silent failures cannot reach 9,700 people

The deliverability hierarchy

Finish this one and you'll have two secondary domains warming, your deliverability caps written into a file that scripts enforce, and a 50-lead pilot campaign created entirely by API with your personalized first lines loaded.

The best email ever written converts at zero if it lands in spam. Deliverability is a stack, and the layers matter in order: infrastructure first, then data quality, then copy, then tricks like spintax. Teams that obsess over copy while sending from their root domain have the hierarchy upside down.

  • Infrastructure - separate sending domains, correct DNS auth, warmed inboxes, sane volume. The foundation; everything below assumes it.
  • Data quality - the <2% bounce rule from lesson 3. Verified emails only, ever.
  • Copy - short, relevant, one ask. Your module-2 work so far.
  • Spintax - per-send phrasing variation so no two emails are byte-identical. Real but last: one study moved inbox placement from 59% to 83% with spintax, ON TOP of solid infrastructure. It rescues nothing on its own.

The numbers that keep you out of spam

These are the 2026 working limits, consistent across the major deliverability sources. Memorize them - they're the difference between a sending engine and a burned domain collection:

  • 20-50 cold sends per inbox per day. 50-100 is the absolute ceiling; the safe cruising range is the lower band. Our own engines run 20-30. Mailbox capacity, not lead count, is the ceiling of every campaign, and a mailbox attached to two campaigns splits its limit between them.
  • 2-3 inboxes per domain. More inboxes on one domain concentrates risk for no gain.
  • Warmup: 3-6 weeks before full volume, starting at 5-20 sends/day and ramping. New inboxes that blast on day one die on day three. If you took lesson 1's buy-domains-now task, yours are mid-warmup already; if not, buy them before doing anything else in this lesson.
  • SPF, DKIM, and DMARC configured on every sending domain. Mandatory, not optional. Google requires SPF or DKIM, a valid PTR record and TLS from every sender, and from bulk senders (5,000+ messages a day to Gmail, counted per primary domain including subdomains) SPF and DKIM and DMARC, an aligned From domain, and one-click unsubscribe honored within 48 hours. Yahoo's rules match. Outlook.com rejects high-volume mail that fails SPF, DKIM or aligned DMARC with 550 5.7.515.
  • The spam-rate numbers Google actually enforces: keep the rate reported in Postmaster Tools below 0.10%, and never reach 0.30%. Google has been ramping up enforcement with temporary and permanent rejections since November 2025, and bulk-sender status is permanent once reached. The 2% bounce rule is vendor guidance that keeps you far away from those two lines.
  • Consistency beats bursts: a steady daily volume reads as a real sender; spiky volume reads as a spammer. Monday is the strongest launch day; Wednesday is the follow-up peak.
  • Sequences: 4-7 touches. 58% of replies come from email #1, 42% from follow-ups - follow-ups nearly double your pipeline, but past touch 7 you're annoying, not persistent.
The classroom math: what 1,000 sends/day actually takes
1,000 sends/day
  at 35-50 sends/inbox/day  ->  ~20-30 inboxes
  at 2-3 inboxes/domain     ->  ~10 domains

Cost: domains (~$12/yr each) + inboxes (~$3-7/mo each)
  + sending platform        ->  roughly $150-250/mo infra

Lead time: 3-6 weeks of warmup BEFORE the first real send.
Buy domains the week you start this course, not launch week.

The sending platform - Instantly as the example

A sending platform owns the machinery you should never rebuild: warmup pools, inbox rotation, bounce processing, scheduling. The course's example is Instantly, because the code below has to be real - but every serious sender exposes the same surface (campaigns, leads, accounts, analytics) through an API, and everything in this lesson maps 1:1 to whatever sender you use. Same move as ever: find your sender's API docs, give Claude Code the key, describe the campaign.

Three things to know about Instantly's API before you build, all of which generalize. First: check the API version - Instantly is v2-only since January 2026, so any tutorial showing v1 endpoints is stale; the current docs live at developer.instantly.ai (they even ship an llms.txt, so Claude can read the docs directly). Whatever sender you use, build against its CURRENT docs, not a blog post. Second: auth is a Bearer token with scopes - create a key with only the scopes your script needs. Third: it has a silent filter. On the lead-list endpoint, campaign_id is silently ignored and the working key is campaign; send the wrong one and you get every lead in the workspace with a 200. Run the lesson-2 check (change the value, watch the count) on the first call you make. As of September 2026 the Growth plan is $47 a month with unlimited sending accounts and warmup.

  • The API surface you need from any sender: campaigns (create, pause, analytics, schedules, daily limits), leads (create, move, list), accounts (sender inboxes, warmup status), and analytics. Confirm yours covers these before building.
  • Many senders also ship an MCP server. Instantly's official hosted one is at mcp.instantly.ai and exposes the v2 API as tools, authenticated with an API key. Use the MCP for inspection and Q&A ("which inboxes bounced most this week?"); use the API for the repeatable builds - the hands/muscle split, as always.

Build: campaign creation end-to-end

The build: a brief goes in, a configured campaign comes out - schedule, sequence, daily caps, and your personalized leads, all via API. No clicking through the UI per campaign.

  1. Set up: domains bought, DNS (SPF/DKIM/DMARC) configured, inboxes created and added to Instantly with warmup enabled. Claude can generate the exact DNS records to paste into your registrar - have it check them with dig afterward.
  2. Write deliverability-defaults.md: your team's caps as config (max sends/inbox/day: 30; sending window: 8am-5pm recipient time, weekdays; warmup gate: no campaign attaches an inbox less than 21 days warm). This file is the source of truth scripts read.
  3. Have Claude write create_campaign.py against developer.instantly.ai: create the campaign with schedule and daily limits from the defaults file, then upload the sequence (4-7 touches), then attach leads.
  4. Generate the sequence copy with spintax variants: "Write touches 1-4 using voice.md; produce 3 spintax variants per sentence so no two sends are identical." Your verified, personalized leads from lesson 6 upload with their first lines as custom variables.
  5. Push 50 leads as a pilot: POST the leads with custom variables, start the campaign, and watch the first 48 hours of stats before loading the rest.
The API call shapes (Instantly v2) - a sketch, not gospel
# Field names and paths drift; have Claude verify each
# endpoint against developer.instantly.ai before building.
import os, requests

BASE = "https://api.instantly.ai/api/v2"
H = {"Authorization": f"Bearer {os.environ['INSTANTLY_API_KEY']}"}
N_MAILBOXES = 6   # inboxes attached to this campaign, from config

# 1. Create the campaign with caps from deliverability-defaults.
#    Two limits exist: the CAMPAIGN daily cap and each inbox's cap.
#    The campaign cap must be sized to mailboxes x per-mailbox cap,
#    or a 20/day default quietly turns a 9,700-lead list into a
#    495-day campaign (pre-launch audit, below).
campaign = requests.post(f"{BASE}/campaigns", headers=H, json={
    "name": "agencies-hiring-sdrs-2026-09",
    "daily_limit": 30 * N_MAILBOXES,   # campaign cap, from defaults
    "campaign_schedule": {"schedules": [{
        "name": "Weekdays",
        "timing": {"from": "08:00", "to": "17:00"},
        "days": {"0": False, "1": True, "2": True, "3": True,
                 "4": True, "5": True, "6": False},
        "timezone": "America/New_York",  # from the docs' enum
    }]},
    # Steps hold variants; each variant has its own subject and body.
    # A blank subject on step 2+ is deliberate: it threads the
    # follow-up as a reply to step 1.
    "sequences": [{"steps": [
        {"type": "email", "delay": 3,
         "variants": [{"subject": "{{signal}} at {{companyName}}",
                       "body": "{{first_line}} ..."}]},
        {"type": "email", "delay": 4,
         "variants": [{"subject": "", "body": "Worth a look?"}]},
    ]}],
}, timeout=30).json()

# 2. Push enriched leads, one POST per lead, personalization as
#    custom variables. The key is "campaign", not "campaign_id" -
#    on the lead-list endpoint the wrong key is silently ignored.
requests.post(f"{BASE}/leads", headers=H, json={
    "campaign": campaign["id"],
    "email": "jane@acme.com",
    "first_name": "Jane",
    "custom_variables": {
        "first_line": "Saw the two SDR roles you posted...",
        "signal": "hiring",
    },
}, timeout=30).raise_for_status()

# 3. Read it all back. Never trust the request; trust the GET.
seqs = requests.get(f"{BASE}/campaigns/{campaign['id']}", headers=H,
                    timeout=30).json()["sequences"]
for seq in seqs:
    for n, step in enumerate(seq["steps"], 1):
        for v in step["variants"]:
            assert v.get("body", "").strip(), f"step {n}: blank body"
            if n == 1:
                assert v.get("subject", "").strip(), "step 1: no subject"

Pre-launch audit: three silent failures in one campaign

A campaign of about 9,700 leads on our own engine was declared "ready". One day before sending, an audit found three failures, none of which had produced an error:

  • Step 2 of the sequence had the body hello!: a placeholder follow-up would have gone to about 9,700 people. Every script that had touched the campaign reported success.
  • The campaign daily_limit was 20, against 145 mailboxes at 30 a day: 4,350 of capacity. At 20 a day the list would have taken 495 days. Both limits looked configured; nobody had compared them.
  • A prune script reported deleted=753 and had deleted nothing. Every DELETE returned 400 (a bodyless DELETE sent with a JSON content type), and the script counted attempts, not successes.

The audit that catches all three is a script, run before every launch, and it reads the truth back from the API rather than trusting what was sent:

  1. Sequence bodies: GET every step and every variant, assert a non-empty body on each and a subject on step 1, and print them; a human reads them once. A blank subject on a later step is normal in Instantly (it threads the follow-up under step 1), so the body is what you check, and the human read is what catches a placeholder like hello!.
  2. Both limits: read the campaign cap and count the attached mailboxes times the per-mailbox cap from deliverability-defaults.md. If the campaign cap is below capacity, fail the audit and print the days-to-complete arithmetic.
  3. Verify deletes and removals: after any prune, GET about 15 of the removed leads and expect 404, and GET about 15 of the kept leads and expect 200. A delete count is only real when the row is gone.
  4. Merge tags: diff the {{variables}} used in the copy against the custom variables present on the leads. A tag with no value renders as a blank in the prospect's inbox.
  5. Blocklist and suppression: run the relationship guard from lesson 4 one more time against the final lead set, and confirm the guard actually read a non-empty list.

Bounces: the sender is a suspect before the list is

When bounces spike, the reflex is to blame the list and re-verify it. On our own engine a verifier-clean list bounced at 5.6% (180 of 3,235). Re-verifying all 180 addresses returned 180 of 180 deliverable. The list was fine. Joined by sending domain, the picture flipped: domains created in February and April bounced at 0.0%; domains created in late July bounced at 13-18%. Receivers were rejecting young domains at about 19 sends per mailbox per day, and in the sender's lead view that looks identical to a bad address.

  • First move on any bounce spike: join bounced leads to the sending account that sent them, roll up by sending domain, and look at the domain age. Only if every domain bounces evenly is the list the suspect.
  • Fix the send, not the list: pull or throttle the young domains and keep the campaign running on the healthy ones. Re-verifying a good list burns money and tells you nothing.
  • Per-domain bounce monitor, the rule we run: remove a sending domain from its campaign when its bounce rate is above 5% with at least 20 sends (below 20, a single bounce is noise). The campaign continues on the other domains; every removal is journaled and posted to Slack. It exists because the sender's own safety net had paused a whole campaign as "accounts unhealthy" when two domains were the problem.
  • Know your sender's fields: in Instantly a lead's esp_code is the RECEIVING provider (1 Google, 2 Microsoft, 3 Zoho, 999 other), not a bounce reason. Segment bounces by it to see whether one receiver is rejecting you.

The rule that protects everything: Claude never sends

The same boundary applies to cadence: Claude Code is not a daemon. Campaigns run inside Instantly on Instantly's schedule; your scripts run on cron to create, feed, and monitor them. Nothing in your stack needs to be awake at 3am.

With sending delegated, your daily job is monitoring. Build the morning deliverability check now - it's 20 minutes and it will save a domain within a month:

  1. Have Claude write morning_check.py: pull account-level warmup and bounce stats plus campaign analytics from the API, and join every bounce to the sending account and domain.
  2. Flag rules: any sending domain above 5% bounces with 20+ sends (pull it from the campaign today, keep the campaign running), any inbox above 2%, warmup score degrading, or reply rate falling off a cliff versus the campaign's trailing average. Add the Postmaster Tools spam rate for each domain when you have it: above 0.10% is a warning line, anything approaching 0.30% is a stop.
  3. Output a 5-line summary. Run it with your coffee; in module 3 it gets scheduled and posted to Slack automatically.

Do this now

Sources and further reading

We set it up with you

Want us to set it up with you, end to end?

Three one-on-one sessions. We train you on your real stack and build your first agents together, until you can run it yourself. You keep everything.