Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

Outbound hygiene

How to Run an Email Deliverability Checklist (Before a Cold Send)

Eight checks, in order. Four are DNS and sending setup. Four are about the list, where most bounces start.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

TL;DR

  • Eight checks: authenticate the domain, warm it, clean the list, pace the send, watch the signals. It takes 45 to 60 minutes the first time.
  • Most bounces come from the list, not the DNS. Filter on emailStatus before you export and you fix the biggest cause for free.
  • LeadOcean records spent: 0 for sizing and counts, 25 if you pull one 25-row test page. The Step 7 export spends one record a row and needs Solo or above.

Prerequisites

  • A sending domain you control, with access to its DNS.
  • A mailbox provider or sequencer that sends from that domain.
  • A LeadOcean API key in $LEADOCEAN_API_KEY. The Free plan covers Steps 1 to 6: 1,000 records for life, no card, 5 requests a second.
  • curl and dig.

Step 1: Authenticate the domain (SPF, DKIM, DMARC)

All three records must exist before the first message. Check them from the shell, not from a vendor dashboard.

bash
dig +short TXT acme.com | grep -i spf
dig +short TXT selector1._domainkey.acme.com
dig +short TXT _dmarc.acme.com

A passing setup returns an SPF string ending in ~all or -all, a DKIM public key, and a DMARC record. Start DMARC at p=none with a report address, then tighten it once reports look clean.

text
v=DMARC1; p=none; rua=mailto:dmarc@acme.com

Step 2: Send from a secondary domain

Never cold send from your primary domain. A look-alike domain such as getacme.com keeps a bad week away from your invoices and support mail.

Redirect the secondary domain's web root to your main site. Add the same three records from Step 1 to it, then confirm they resolve:

bash
for d in getacme.com _dmarc.getacme.com selector1._domainkey.getacme.com; do
  echo "$d: $(dig +short TXT $d)"
done

An empty answer on any line means a record is missing or not yet propagated.

Step 3: Warm up before volume

A new mailbox has no reputation. Start with a few dozen messages a day to people likely to reply, and raise the volume over weeks, not days.

Write the ramp down before you start, so the daily cap is never a judgement call. These numbers are our illustration, not a provider rule.

yaml
mailbox: jane@getacme.com
week_1: { per_day: 10 }
week_2: { per_day: 20 }
week_3: { per_day: 30 }
advance_only_if: { hard_bounces: "under 2%", spam_complaints: 0 }

Warm-up is a sequencer or mailbox-tool feature. LeadOcean does not send email, so use the tool you already send with.

Step 4: Size the list for free, and read the mailable gap

Count before you pull anything. The count call is free. With no emailStatus in the body, the REST count covers everyone who matches the role.

bash
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" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"]}'

The MCP tool leadocean_count_leads defaults to addresses worth sending to (verified, catch_all_valid and catch_all) and also returns totalAll, the people with no email condition. The gap between them is the part you should not send to. meta.total on REST is capped at 100,000.

Step 5: Filter on email_status, strictest first

emailStatus takes exact strings. The 13 values are verified, catch_all_valid, catch_all, risky, unknown, untested, invalid, role, disposable, spam_trap, abuse, derived and none.

For a first send from a young domain, use verified only.

bash
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" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"]}'

Widen to catch_all_valid once the domain has history. Do not send to risky, invalid, role, disposable, spam_trap or abuse. LeadOcean gives no refund or credit for bounced emails, so read the status before you send.

Step 6: Pull a small test batch and check the fields

Spend 25 records, not 25,000, to inspect real rows. Search body is flat JSON, filters are arrays, and limit runs 1 to 100.

bash
curl -s https://api.leadocean.io/v1/people/search \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"],"limit":25}'

Search rows carry flags, not addresses. Check that each row has has_email: true, that email_status is verified, and that the company in contact_current_experiences fits your target. A mismatch means your filter is too loose. A search status is an upper bound; the address and its confirmed status come back at enrichment or export.

Step 7: Export the clean list and pace the send

Use the Exports page in app.leadocean.io (filter builder, column picker, price preview, CSV download) or POST /v1/exports for the same list. Bulk CSV export is not on Free. A request takes up to 50,000 rows, with 500,000 a day.

Request email_1_address and email_1_status so the status travels with the row into your sequencer.

bash
curl -s -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"ca-vp-sales-verified","limit":1854,"filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"]},"columns":["profile_first_name","current_company_name","email_1_address","email_1_status"]}'

Every row costs one record. Then cap daily volume per mailbox well below what the mailbox can technically send, and spread sends across mailboxes.

Step 8: Monitor and set stop rules

Decide the stop rule before you send, not after. Pause a mailbox when bounces or spam complaints jump above your own baseline, and fix the cause before resuming. Our default, as config:

yaml
pause_mailbox_if:
  hard_bounce_rate: "over 2% of the day's sends"
  spam_complaints: "above your baseline in Postmaster Tools"
  dmarc_failures: "any new failing source"

Watch three signals: bounce rate, spam complaints in Google Postmaster Tools, and DMARC aggregate reports. Remove every bounced address from future lists and re-count your segment each month, because the dataset is refreshed monthly and each record carries its own fetched_at date.

What you get

A free count for the same audience, run on 2026-10-21 with leadocean_count_leads (VP, Sales & Business Development, country CA). The first row is the tool's mailable default. The second adds emailStatus: ["verified"].

text
filters                                   total   totalAll   distinctCompanies
default (verified+catch_all_valid+catch_all)  5,574   18,800     3,873
emailStatus: verified                         1,854   18,800*    1,392

*totalAll is the same filters with no email condition, as reported in the default call. Records spent: 0.

So 18,800 people match the role, 5,574 are mailable by the default rule, and 1,854 are verified. Starting a young domain on the 1,854 is the safe version of this send.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerSend the key from $LEADOCEAN_API_KEY in the x-api-key header
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour Retry-After, or slow the loop
402The Free plan's 1,000 records are spentCounts stay free; move to Solo at $97 a month for more
Empty resultsA filter value is not an exact enum stringCheck leadocean_list_enum_values for email_status and job_level
Export refusedOver 50,000 rows a request or 500,000 a daySplit the list into several exports
404 on enrichNobody matched the inputCosts nothing; try another identifier

FAQ

Does a verified status guarantee delivery?

No. It means the address checked out when it was verified. Content, domain reputation and sending pace still decide inbox placement.

Should I send to catch_all addresses?

Only once the domain has a clean history. A catch-all server accepts everything, so a bad address can still bounce later. Start with verified.

How often should I re-check a list?

Re-count and re-pull before every campaign. The dataset is refreshed monthly, so a list saved last quarter is stale.

Does LeadOcean send or warm up email?

No. It supplies the data and the email_status field. Sending and warm-up happen in your sequencer. For related reading, see how to improve email deliverability, deliverability best practices for B2B and building for email deliverability.

What does it cost?

Counts are free. The Free plan gives 1,000 records for life, and paid plans start at $97 a month. See pricing.

Back to the blog hub.

Count your mailable list before you send a single email

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →