TL;DR
POST /v1/exportsqueues an export of up to 50,000 rows. You pollGET /v1/exports/{id}until it is done, then download the CSV.- Setup takes about 15 minutes. The run time depends on the queue and the row count (both are estimates).
- It writes up to 50,000 rows. Each row counts 1 record, or 2 when the columns need an enrichment, as the
fullpreset does (perRowin 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.
The script below is 37 lines of bash. It retries on 429, reads Retry-After, and writes leads.csv. If you prefer a typed client, see the Go, C# or TypeScript versions. The full picture is in the bulk CSV export hub.
Prerequisites
- A LeadOcean account on Solo or Agency. Bulk CSV export is not on Free.
- An API key with the
searchandenrichscopes, exported as$LEADOCEAN_API_KEY. The defaultfullcolumn preset reveals emails and phones, so it needsenrich. curlandjq, plus bash 4 or later.
Read the export endpoints in the API reference once (checked October 2026).
Step 1: Size the list for free
A count costs nothing. Send count=true as a query parameter with limit=1, and the body as flat JSON with array filters.
curl -s -X POST "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"],"country":["US"]}' | jq .meta.totalmeta.total is capped at 100,000. If it is under your target, the export writes fewer rows than you asked for. Enum values are exact strings, so VP works and vp does not.
Step 2: Save the script
Save this as export.sh. It runs one request helper, then create, poll and download.
#!/usr/bin/env bash
set -euo pipefail
API=https://api.leadocean.io
TMP=$(mktemp -d)
call() { # call METHOD PATH [JSON]: prints the body, retries on 429
local tries=0 code wait
while :; do
code=$(curl -s -o "$TMP/body" -D "$TMP/head" -w '%{http_code}' -X "$1" "$API$2" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" ${3:+-d "$3"})
if [ "$code" = 429 ] && [ "$tries" -lt 5 ]; then
wait=$(awk 'tolower($1)=="retry-after:"{print $2+0}' "$TMP/head")
[ "${wait:-2}" -le 120 ] || { echo "429, retry in ${wait}s. Stop here." >&2; exit 1; }
sleep "${wait:-2}"; tries=$((tries+1)); continue
fi
[ "$code" -lt 300 ] || { echo "HTTP $code: $(cat "$TMP/body")" >&2; exit 1; }
cat "$TMP/body"; return
done
}
BODY='{"name":"vp-us-50k","limit":50000,"filters":{"jobLevel":["VP"],"country":["US"]}}'
ID=$(call POST /v1/exports "$BODY" | jq -r .data.id)
echo "queued $ID"
while :; do
EXP=$(call GET "/v1/exports/$ID")
STATUS=$(jq -r .data.status <<<"$EXP")
echo "$STATUS rows=$(jq -r .data.rows <<<"$EXP")"
case $STATUS in
done) break ;;
failed|cancelled) jq .data.error <<<"$EXP"; exit 1 ;;
esac
sleep "$(jq -r '.data.pollAfter // 10' <<<"$EXP")"
done
call GET "/v1/exports/$ID/download" > leads.csv
wc -l leads.csvThe 429 branch reads Retry-After and waits. It gives up after 5 tries, or if the wait is over 2 minutes. A long wait usually means the daily export ceiling, which a retry will not fix.
Step 3: Run it
export LEADOCEAN_API_KEY="paste-your-key-here"
chmod +x export.sh
./export.shOne export runs per account at a time. Others queue. pollAfter tells the loop how long to sleep, so you do not hammer the endpoint. On Free the key allows 5 requests a second, on Solo 50, on Agency 200. A polling loop uses far less than that.
Step 4: Page through a bigger list
One export holds 50,000 rows and one day holds 500,000. For a larger audience, split it by filter and run the script once per slice. Change country, jobFunction or employeeRange in BODY so the slices do not overlap, then merge the files.
head -n 1 leads_1.csv > all.csv
for f in leads_*.csv; do tail -n +2 "$f" >> all.csv; doneRows inside one export are deduplicated by person_id. Across files, dedupe on that column yourself.
What you get
./export.sh prints the queue and poll lines, then the line count. Output shape, with placeholder values:
queued exp_acme123
queued rows=0
running rows=18420
running rows=41000
done rows=50000
50001 leads.csvThe create call answers 202. This is the response shape from the API reference, with placeholder values:
{
"success": true,
"data": {
"id": "exp_acme123",
"name": "vp-us-50k",
"status": "queued",
"requested": 50000,
"perRow": 2,
"remaining": 120000,
"pollAfter": 10
}
}The CSV has a header row, then one person per row. Numbered columns come back as email_1_address, email_1_status, phone_1_number and so on. Check email_1_status before you send anything. LeadOcean does not refund bounces. The file is kept for 7 days.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is set in this shell and the key is not revoked |
| 403 | Contact columns asked without the enrich scope | Create a key with both search and enrich, or set "preset":"search" |
| 402 | Fewer records left than the export needs | Lower limit to the remaining value in the error |
| 429 | Above your plan's rate limit (5 a second on Free, 200 on Agency), or the daily export ceiling | Wait for Retry-After. For the daily ceiling, wait for resetsAt |
| 400 | limit above 50,000, or a filter an export cannot apply | Set limit to 50,000 or less. Read the export_filter_unsupported message |
| Rows below target | The filter matches fewer people than limit | Run the free count in Step 1 and loosen the filters |
FAQ
How many rows can one cURL export return?
50,000 rows per request and 500,000 per day per account. The create call reserves records up front and gives back what it does not write.
Does a count cost records?
No. A search with count=true and limit=1 is free, as is GET /v1/exports/columns. Use them before every export.
Can I pick the columns?
Yes. Pass columns as a list of ids from GET /v1/exports/columns, or a preset of search or full. The create response shows perRow: 1 record a row, or 2 when the columns need an enrichment.
How long is the download link valid?
The file stays for 7 days. downloadUrl is a signed link that works without a key for 24 hours, and reading the export again mints a fresh one for free.
Do I need the app instead?
Not if you want a script. The Exports page in app.leadocean.io does the same job with a filter builder, a column picker and the record price shown before you start. Plans are on the pricing page.
Run your first export from the command line
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →