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

How-to guide

How to Automate Outbound (with a Weekly Export Job)

Size the ICP for free, export new mailable people on a schedule, drop anyone you already contacted and hand a clean CSV to your sequencer.

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

TL;DR

  • You build one script that runs weekly: count the ICP, export mailable people, drop anyone already contacted, write a CSV your sequencer can import.
  • It takes about 30 minutes to build. The 40-person example spends up to 80 of your records, because a row costs 1 record, or 2 when its columns need an enrichment (email columns do). Sizing is free.
  • LeadOcean supplies the people and the email status. It does not send mail. Your sequencer sends.

Guides such as Clay's outbound guide (checked October 2026) explain the idea of signal-led sequences. This page is the runnable version for a technical buyer: real queries, real output shape, no screenshots.

Prerequisites

  • A LeadOcean API key from app.leadocean.io, exported as $LEADOCEAN_API_KEY. Bulk CSV export is not on Free. Solo ($97 a month) includes it. On Free you can still run Steps 1 and 2, and replace Step 3 with a small people search.
  • Python 3.9 or later with pip install requests.
  • A sequencer or CRM that imports a CSV. This guide never touches it, so any will do.
  • A machine that stays on at the scheduled time (cron on macOS or Linux).
bash
export LEADOCEAN_API_KEY="paste-your-key-here"
mkdir outbound-job && cd outbound-job

Step 1: Write the ICP as a filter file

Keep the ICP in one JSON file so the count, the export and the next person who reads the repo all use the same definition. The body is flat JSON and every filter is an array.

json
{
  "jobLevel": ["VP"],
  "jobFunction": ["Sales & Business Development"],
  "country": ["US"],
  "employeeRange": ["51-200"],
  "technologyCategories": ["CRM"],
  "emailStatus": ["verified", "catch_all_valid", "catch_all"]
}

Save it as icp.json. Enum values are exact strings. If you are unsure of one, look it up first, which is free.

bash
curl -s "https://api.leadocean.io/v1/enums/job_function" \
  -H "x-api-key: $LEADOCEAN_API_KEY" | head -c 400

Step 2: Size the audience for free

Count before you export. A count spends no records, however often you run it. Send count=true as a query parameter with limit at 1.

bash
curl -s "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d "$(jq '. + {limit: 1}' icp.json)" \
  | jq '.meta | {total, totalIsExact, distinctCompanies, countOnly}'

meta.total is capped at 100,000, so a bigger audience reads as a floor. If the number is too large for one export (50,000 rows), narrow the filters. If it is under 50, widen the title or headcount band.

Step 3: Export the people

POST /v1/exports takes your filters, a row limit and a column list. Ask only for the columns your sequencer needs. One export runs per account at a time, and each export is capped at 50,000 rows and 500,000 rows a day.

bash
curl -s https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d "$(jq -n --slurpfile f icp.json '{
    name: "weekly-vp-sales",
    filters: $f[0],
    limit: 500,
    caps: {emails: 1},
    columns: ["person_id","profile_first_name","profile_last_name",
              "current_job_title","current_company_name","current_company_domain",
              "email_N_address","email_N_status"]
  }')" | jq '.data | {id, status, requested, perRow, remaining}'

Numbered columns are requested as email_N_address and email_N_status, with caps.emails setting how many. The CSV headers come back as email_1_address and email_1_status. The call is accepted or refused on the spot. It reserves records for up to limit rows, bills each row written and gives the rest back.

Step 4: Write the weekly job

Save this as weekly_outbound.py. It creates the export, polls until it finishes, downloads the CSV and drops every person_id you have already sent. sent.txt is your memory between runs.

python
import csv, json, os, time, requests

BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}
BASE_COLS = ["person_id", "profile_first_name", "profile_last_name",
             "current_job_title", "current_company_name", "current_company_domain"]
REQUEST_COLS = BASE_COLS + ["email_N_address", "email_N_status"]
CSV_COLS = BASE_COLS + ["email_1_address", "email_1_status"]

filters = json.load(open("icp.json"))
sent = set(open("sent.txt").read().split()) if os.path.exists("sent.txt") else set()

r = requests.post(f"{BASE}/v1/exports", headers=H, json={
    "name": time.strftime("weekly-%Y-%m-%d"), "filters": filters,
    "limit": 500, "caps": {"emails": 1}, "columns": REQUEST_COLS})
r.raise_for_status()
exp = r.json()["data"]

while exp["status"] in ("queued", "running"):
    time.sleep(exp.get("pollAfter") or 5)
    exp = requests.get(f"{BASE}/v1/exports/{exp['id']}", headers=H).json()["data"]
if exp["status"] != "done":
    raise SystemExit(f"export {exp['status']}: {exp.get('error')}")

body = requests.get(f"{BASE}/v1/exports/{exp['id']}/download", headers=H).text
rows = [x for x in csv.DictReader(body.splitlines()) if x["person_id"] not in sent]

out = time.strftime("import-%Y-%m-%d.csv")
with open(out, "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=CSV_COLS, extrasaction="ignore")
    w.writeheader()
    w.writerows(rows)
with open("sent.txt", "a") as f:
    f.writelines(x["person_id"] + "\n" for x in rows)
print(f"{len(rows)} new people written to {out}")

Run it once by hand and read the CSV before you schedule anything. Add sent.txt entries from your CRM first if you have existing contacts.

Step 5: Gate the file before it ships

An unattended job needs a gate. Add a check that stops the hand-off when the file looks wrong, so a bad week never reaches a sequencer. Save this as gate.py and run it after the job.

python
import csv, glob, sys

path = sorted(glob.glob("import-*.csv"))[-1]
rows = list(csv.DictReader(open(path)))
ok = {"verified", "catch_all_valid", "catch_all"}

bad = [r for r in rows if r["email_1_status"] not in ok or not r["email_1_address"]]
domains = {r["current_company_domain"] for r in rows}

print(f"{path}: {len(rows)} rows, {len(domains)} companies, {len(bad)} unmailable")
if not rows or bad:
    sys.exit(1)

A non-zero exit means: do not import, and remove that week's IDs from sent.txt before the next run. A zero-row week usually means the audience is spent, which is a signal to change the slice, not a failure of the job. Keep a cap per company too. Ten people from one company in one week is a list problem, not a volume win.

Step 6: Schedule it and hand off

Cron runs the script every Monday at 06:00. Then import import-YYYY-MM-DD.csv into your sequencer. Most sequencers can watch a folder or accept a CSV upload. Check your own tool's docs for that step.

bash
crontab -e
0 6 * * 1 cd $HOME/outbound-job && (python3 weekly_outbound.py && python3 gate.py) >> job.log 2>&1

Cron does not load your shell profile, so set LEADOCEAN_API_KEY at the top of the crontab or in a secrets file. Never commit it to the repository. Read email_1_status before you send. Bounced emails are not refunded.

What you get

Step 2 returns a real number. On 2026-10-21 the free MCP count (leadocean_count_leads, default email filter) for this ICP returned the result below. Filters: VP, Sales & Business Development, US, 51-200 employees, CRM detected on the site.

json
{
  "total": 40,
  "totalIsExact": true,
  "totalAll": 135,
  "distinctCompanies": 30,
  "credits": 0,
  "defaults": "Counted only addresses worth sending to (emailStatus: verified or catch_all_valid or catch_all)"
}

That is 40 mailable people across 30 companies. The other 95 of the 135 have no verified or catch-all address.

A finished export, as the response shape from the API reference with placeholder values:

json
{
  "success": true,
  "data": {
    "id": "exp_123",
    "name": "weekly-vp-sales",
    "status": "done",
    "requested": 500,
    "estimatedTotal": 40,
    "rows": 40,
    "billed": 40,
    "released": 460,
    "pollAfter": null,
    "downloadUrl": "https://api.leadocean.io/v1/exports/exp_123/download?t=..."
  }
}

The CSV has one row per person. One placeholder row:

csv
person_id,profile_first_name,profile_last_name,current_job_title,current_company_name,current_company_domain,email_1_address,email_1_status
p_123,Jane,Doe,VP Sales,Acme,acme.com,jane.doe@acme.com,verified

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCopy the key again from app.leadocean.io and re-export $LEADOCEAN_API_KEY.
429 on /v1/exportsThe 500,000-row daily export ceiling (or the period ceiling) is reachedWait until resetsAt in the response. Smaller weekly slices stay far under it.
429 on other callsAbove the plan's rate limit (5 requests a second on Free, 200 on Agency)Wait the Retry-After seconds. The job makes few calls, so this is rare.
402Fewer records left than the export needsLower limit, or check GET /v1/account. Free records do not reset.
400 export_limit_rowslimit is above 50,000Set limit to 50,000 or less and split the audience by country or headcount band.
Empty file or 400 on filtersAn enum value that does not match, such as "VP Sales" in jobLevelResolve the exact string with GET /v1/enums/<name> and use it.

FAQ

Does LeadOcean send the emails?

No. It supplies people, work emails and a deliverability label. Sending and sequencing belong to your sequencer.

Why track person_id instead of email?

An address can change between runs. person_id is the stable key, and an export dedupes rows by it.

How do I avoid pulling the same people every week?

An export always starts at the top of its filter set, so the same people come back. Dedupe against sent.txt as in Step 4. When most rows are repeats, change the slice (a new country or headcount band) instead of raising limit.

Can I trigger this from Claude Code or n8n instead of cron?

Yes. The job is plain HTTP, so any scheduler that can run a script or call POST /v1/exports works. LeadOcean also has a hosted MCP server with 13 tools, and the count tool is free. See outbound lead generation and warm outbound for the terms, outbound sales automation for the wider stack, and pricing for plans.

More recipes live in the blog hub.

Start your weekly outbound export with a free API key

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

Get your free API key →