To warm up a domain, authenticate it and send a few wanted emails a day. Raise the volume in steps while bounces stay near zero. The schedule is the easy part. The list you send to is the part that decides the outcome.
This guide gives you a four-week plan as code. It uses LeadOcean for the one job it can do: building a verified-only list and slicing it into daily batches. LeadOcean does not send email or warm mailboxes. For that you need your sending tool.
TL;DR
- Takes about 45 minutes to set up and four weeks to ramp. Daily work is a batch upload and a glance at your bounce count.
- Costs 0 records to size the list and 650 records to export it for two mailboxes on the example ramp. Bulk CSV export needs Solo ($97 a month); it is not on Free.
- Google says to increase volume slowly and monitor spam rate and domain reputation (Google, October 2026). The numbers below are a starting plan, not an official schedule.
Prerequisites
- A sending domain you control, plus two or more mailboxes on it.
- Access to DNS for that domain, and
digon your machine. - A LeadOcean API key exported as
$LEADOCEAN_API_KEY. Get one free at app.leadocean.io. - Python 3 for the batch script in Step 4.
- A sending tool that can import a CSV and send on a daily cap.
Step 1: Authenticate the domain
Authentication comes before the first email. Google requires SPF or DKIM for all senders. Bulk senders need SPF, DKIM and DMARC (Google, October 2026). Publish all three.
Check what is live:
dig +short TXT acme-outbound.com | grep spf
dig +short TXT selector1._domainkey.acme-outbound.com
dig +short TXT _dmarc.acme-outbound.comYou want one SPF record, one DKIM key and a DMARC policy. Start DMARC at p=none with a reporting address so you can read failures, then tighten it later.
v=DMARC1; p=none; rua=mailto:dmarc@acme-outbound.comSending to personal Gmail accounts needs a DKIM key of 1024 bits or longer. Google recommends 2048 bits where your provider supports it (Google, October 2026).
Authentication errors during warm-up are expensive. A message that fails SPF and DKIM can be marked as spam or rejected with a 5.7.26 error (Google, October 2026). Send one test message to a mailbox you own and read the headers before you import a single contact.
Step 2: Size a verified-only list for free
Count first. A count spends nothing and tells you whether the audience is big enough for four weeks of sends.
Warm-up volume is small, so you want your best addresses only. Filter emailStatus to verified. The default count includes catch_all_valid and catch_all too, and you do not want those in week one.
Call the free MCP tool from Claude:
leadocean_count_leads
jobLevel: ["VP"]
jobFunction: ["Sales & Business Development"]
country: ["CA"]
emailStatus: ["verified"]Or use REST with a count-only search. Send count=true as a query parameter, with limit 1:
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-X POST \
-d '{
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailStatus": ["verified"]
}'meta.total is capped at 100,000, and the call costs no records.
Step 3: Export the warm-up list
Export once, with a limit that matches your ramp, not a round number. The example ramp below sends 650 emails, so ask for 650 rows.
curl -s https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "warmup-ca-vp-sales",
"limit": 650,
"preset": "full",
"caps": {"emails": 1, "phones": 1},
"filters": {
"jobLevel": ["VP"],
"jobFunction": ["Sales & Business Development"],
"country": ["CA"],
"emailStatus": ["verified"]
}
}'Poll GET /v1/exports/{id} until it finishes, then fetch GET /v1/exports/{id}/download. Every row costs one record, and the export is refused up front if the account cannot cover it. The CSV headers come back as email_1_address, email_1_status, and so on.
On the Free plan, bulk export is not available. The Free route is POST /v1/people/search for person ids, then POST /v1/people/enrich with reveal_email: true for each one. That costs two records a person, so Free's 1,000 covers about 500 people: enough for weeks one to three, not the full ramp. On Solo, the Exports page in app.leadocean.io shows the record price before you start.
Step 4: Slice the list into the ramp
A ramp is a table of numbers. Put it in code so the daily batch is never a judgement call. This plan is two mailboxes, weekdays only.
| Week | Per mailbox per day | Both mailboxes per day | Emails in the week |
|---|---|---|---|
| 1 | 5 | 10 | 50 |
| 2 | 10 | 20 | 100 |
| 3 | 20 | 40 | 200 |
| 4 | 30 | 60 | 300 |
That is 650 emails. These are illustrative numbers. Providers publish no official schedule, so adjust to what your results show.
import csv
RAMP = {1: 10, 2: 20, 3: 40, 4: 60} # emails per day, both mailboxes
MAILBOXES = ["jane@acme-outbound.com", "sam@acme-outbound.com"]
with open("warmup-ca-vp-sales.csv", newline="") as f:
rows = [r for r in csv.DictReader(f) if r["email_1_status"] == "verified"]
day, i = 0, 0
for week, per_day in RAMP.items():
for d in range(5): # Monday to Friday
day += 1
batch = rows[i : i + per_day]
i += per_day
for n, row in enumerate(batch):
row["sender"] = MAILBOXES[n % len(MAILBOXES)]
with open(f"day-{day:02d}.csv", "w", newline="") as out:
w = csv.DictWriter(out, fieldnames=list(batch[0].keys()))
w.writeheader()
w.writerows(batch)
print(f"{i} rows sliced into {day} daily files")Upload one day-NN.csv a day. Do not skip the filter on email_1_status: it keeps a stray catch_all row out of week one.
Two habits keep the plan honest. Send from each mailbox at a steady time of day, not in one burst. Keep each message short, plain text, one question, no tracking-heavy templates in the first two weeks. Warm-up volume is small, so the content of each email matters more than it will at scale.
Step 5: Set the gates
Move to the next week only when the last week was clean. These gates are our rule of thumb, not a provider rule.
Advance a week when ALL of these hold:
hard bounces under 2% of the week's sends
spam complaints none you can see in Postmaster Tools
auth failures none in the DMARC reports
replies / opens steady, not collapsing
If any fails: repeat the week at the same volume. Do not step back up yet.Google tells senders to monitor server responses, spam rate and domain reputation as they raise volume (Google, October 2026). Postmaster Tools is the place to watch it. Keep sending on weekdays even after week four. A domain that goes quiet loses its rhythm.
Step 6: Hold a floor after week four
Do not jump from 60 sends a day to 600 on day 21. Google says more volume means slower increases. It also says daily sending lets you raise volume faster than weekly sending (Google, October 2026). Add 20 to 30 percent a week, re-check the gates, and widen the list in this order:
- Keep
emailStatus: ["verified"]and add a second country or a second job function. - Add
catch_all_validonce week-four bounces sit under your gate. - Add
catch_alllast, in a separate batch, so you can see its bounce rate on its own.
Re-run the count from Step 2 for each new slice. It is free, and it tells you whether the slice is big enough to be worth a batch.
What you get
The count from Step 2, run on 2026-10-21 with the free leadocean_count_leads tool:
{
"total": 1854,
"totalIsExact": true,
"distinctCompanies": 1392,
"credits": 0
}Filters: jobLevel VP, jobFunction Sales & Business Development, country CA, emailStatus verified. That is 1,854 people with a verified status across 1,392 companies. A search status is an upper bound, which is why Step 4 re-checks email_1_status in the file. It is enough for the 650-email ramp with room to spare, and it cost nothing.
The sliced output from Step 4 is a folder of daily files. This is the shape, with placeholder values:
$ python3 slice.py
650 rows sliced into 20 daily files
$ wc -l day-01.csv day-20.csv
11 day-01.csv
61 day-20.csvEach file has a header row, so day 1 has 10 sends and day 20 has 60.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header. | Re-export $LEADOCEAN_API_KEY from app.leadocean.io and resend. |
| 429 | Requests above the plan's rate limit (5 a second on Free, 50 on Solo, 200 on Agency). | Honour the Retry-After header and slow the loop. |
| 402 | Out of records. A Free account has 1,000 for life. | Add a plan, or cut the export limit. The export is refused before it starts. |
| Zero results | A filter value is not an exact enum string, such as vp where jobLevel wants VP. | List valid values with leadocean_list_enum_values or GET /v1/enums. |
400 export_limit_rows | The export limit is above the 50,000 rows a request. | Lower limit, or split the pull into several exports. |
429 export_limit_daily | You passed 500,000 export rows in a day. | Run the rest of the export the next day. |
FAQ
How long does a domain warm up take?
Plan on four weeks for a few mailboxes at a few dozen sends a day. Providers publish no official number. A domain with early bounces needs longer.
Do I need a warm-up tool?
No. A tool automates mailbox-to-mailbox traffic. You can warm a domain by sending real one-to-one email to real, verified contacts and ramping slowly. A tool does not fix a bad list or missing authentication.
Why filter to verified only?
Early bounces do the most damage on a domain with no history. The default count includes catch_all_valid and catch_all. Use emailStatus: ["verified"] for the warm-up weeks and widen it once the domain is established.
Does LeadOcean warm my domain?
No. LeadOcean has no sending, sequencer or mailbox warm-up. It builds the list and tells you each address's email_status. Pair it with a sender. See also inbox warm up, inbox warming and Gmail warmup.
What does it cost?
Sizing is free. The 650-row export costs 650 records. Free gives you 1,000 records for life, but bulk export needs Solo at $97 a month. Search plus enrich on Free costs two records a person. See pricing.
Build your warm-up list from verified addresses only
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →