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

How-to guide

How to Export 50,000 Leads to CSV (Python)

One POST starts the job, one loop polls it, one GET streams the file. Here is the whole script.

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

You export 50,000 leads from Python with one POST /v1/exports, a polling loop and a streamed download. The server walks the list, so you never page through results yourself.

TL;DR

  • The whole script is 42 lines of Python 3 and the requests package.
  • Setup takes about 10 minutes. The export runs in the background and tells you how long to wait between polls (pollAfter).
  • It writes up to 50,000 rows. Each row counts 1 record, or 2 when the columns need an enrichment, as the full preset does (perRow in the 202 response says which).
  • Bulk CSV export is not on Free. On Solo ($97 a month) a 50,000-row export with contact columns passes the pace of 10k enrichments every 5 hours and slows down. Agency ($497 a month) fits this size.

Prerequisites

  • Python 3.9 or later and pip install requests.
  • A LeadOcean API key on a paid plan, exported as $LEADOCEAN_API_KEY. Create an account at app.leadocean.io.
  • A filter set. Size it first with the free count call so you know the export will fill.

Step 1: Size the list for free

Counting spends no records. Run it before you reserve 50,000.

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":["US"]}'

meta.total is capped at 100,000. The MCP tool leadocean_count_leads does the same job. Enum values are exact strings, so check them with GET /v1/enums before you guess.

Step 2: Create the export with retries on 429

A 429 means you sent requests faster than your plan allows: 5 a second on Free, 50 on Solo, 200 on Agency. The response carries Retry-After. The helper honors it and gives up after 5 tries.

Create export.py:

python
import os, sys, time, requests

http = requests.Session()
http.headers["x-api-key"] = os.environ["LEADOCEAN_API_KEY"]
BASE = "https://api.leadocean.io"

def call(method, path, **kw):
    for attempt in range(6):
        r = http.request(method, BASE + path, **kw)
        if r.status_code != 429 or attempt == 5:
            return r
        time.sleep(float(r.headers.get("Retry-After", 2 ** attempt)))

body = {
    "name": "vp-sales-us",
    "filters": {
        "jobLevel": ["VP"],
        "jobFunction": ["Sales & Business Development"],
        "country": ["US"],
    },
    "limit": 50000,
    "preset": "full",
}
r = call("POST", "/v1/exports", json=body)
if not r.ok:
    sys.exit(f"{r.status_code} {r.text}")
exp = r.json()["data"]

The filters object takes any people-search filter except cursor, limit, count and facets. The full preset has 45 columns and needs the enrich scope. The search preset has 27 columns and no contact points. perRow in the 202 response says what each row counts: 1, or 2 when the columns need an enrichment.

Step 3: Poll until the job is done

The create call answers 202 with the export object. Wait pollAfter seconds, then GET /v1/exports/{id} until status leaves queued and running. One export runs per account at a time, so a second job waits in the queue.

Append to export.py:

python
while exp["status"] in ("queued", "running"):
    time.sleep(exp.get("pollAfter") or 5)
    r = call("GET", f"/v1/exports/{exp['id']}")
    r.raise_for_status()
    exp = r.json()["data"]
    print(exp["status"], exp["rows"], "rows")

Step 4: Stream the file to disk

When status is done, download the CSV. The file stays available for 7 days. A 409 means the job is still running and a 410 means the file expired.

Append to export.py:

python
if exp["status"] != "done":
    sys.exit(f"Export ended as {exp['status']}: {exp.get('error')}")

with call("GET", f"/v1/exports/{exp['id']}/download", stream=True) as r:
    r.raise_for_status()
    with open("leads.csv", "wb") as f:
        for chunk in r.iter_content(1 << 16):
            f.write(chunk)
print("Saved leads.csv")

Run it with python export.py. Rows are deduplicated by person_id, so the file can hold fewer rows than you asked for. You are billed for rows written, and the unused reservation comes back.

What you get

A free count for the Step 1 filters on 2026-10-21, from leadocean_count_leads:

json
{ "total": 129494, "totalIsExact": true, "totalAll": 350517, "distinctCompanies": 58121 }

total counts people with a verified, catch-all-valid or catch-all email (the tool's default). totalAll is the same filters with no email condition. Either figure is above 50,000, so the export fills.

The export object, response shape from the API reference, placeholder values:

json
{
  "success": true,
  "data": {
    "id": "exp_acme123",
    "name": "vp-sales-us",
    "status": "queued",
    "requested": 50000,
    "rows": 0,
    "billed": 0,
    "pollAfter": 5
  }
}

The CSV headers for numbered blocks come back as email_1_address, email_2_address and phone_1_number.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key.Check $LEADOCEAN_API_KEY is set in the shell that runs python export.py.
429 with Retry-AfterRequests above your plan's rate: 5 a second on Free, 200 on Agency.Let the retry helper wait. Poll at pollAfter, not in a tight loop.
429 export_limit_daily or export_limit_monthlyExport ceiling reached (500,000 rows a day).Do not retry. Read resetsAt in the body and run the next export then.
402Fewer records left than the export reserves.Lower limit, or check the balance with GET /v1/account.
400 export_limit_rowslimit is above 50,000.Split the list across several exports with narrower filters.
Empty file or 0 rowsA filter value that is not an exact enum string.Look it up with GET /v1/enums and fix the casing.

FAQ

Can I export more than 50,000 leads?

Yes, in several exports. Each request is capped at 50,000 rows and each account at 500,000 rows a day. Split the list by country or seniority so the filter sets do not overlap.

Does the export spend records if the list is smaller than my limit?

You are billed for rows written. The reservation is up to limit records at once, and what the job does not write is released when it ends.

Can I run this on the Free plan?

No. Free has 1,000 records for life and no bulk CSV export. Use search or enrich on Free, then move to Solo for exports. See pricing.

Do I need to page through results?

No. Your script polls one export and downloads one file. For the same recipe in other languages, see the Go, C# and TypeScript versions, or the bulk CSV export overview.

Export your first lead list from Python

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

Get your free API key →