Send each URL to POST /v1/people/enrich in a loop, back off when the API answers 429, and append every hit to a file. The whole script is under 30 lines of bash.
TL;DR
- Time: about 10 minutes to set up. On the Free plan the loop runs at most 4 profiles a second, so 100 URLs take at least 25 seconds.
- Cost: one LeadOcean record per profile found. A profile we hold nothing for is a 404 and costs nothing.
- Output: one JSON line per person in
results.jsonl, and a separatemisses.txtfor 404s.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The Free plan gives 1,000 records for life, no card.
- The key in your shell as
$LEADOCEAN_API_KEY. curlandjq.- A file
urls.txtwith one LinkedIn profile URL per line.
Step 1: Set the key and test one URL
Export the key, then enrich a single profile. The header is x-api-key and the base URL is https://api.leadocean.io.
export LEADOCEAN_API_KEY="your_key_here"
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"linkedin_url": "https://www.linkedin.com/in/jane-doe-example"}'The body takes exactly one of linkedin_url, email, phone or person_id. Add "reveal_email": true to get the emails and phones we hold, at no extra record.
Step 2: Loop over the list with retries on 429
Free keys allow 5 requests a second, Solo 50, Agency 200. Above that the API answers 429 with a Retry-After header. The loop below sleeps 0.25 seconds between calls (4 a second, under the Free limit) and honours Retry-After if it fires.
#!/usr/bin/env bash
set -u
API=https://api.leadocean.io/v1/people/enrich
: > results.jsonl; : > misses.txt
enrich() {
local url="$1" tries=0 code wait
while [ "$tries" -lt 5 ]; do
code=$(curl -s -o body.json -D headers.txt -w '%{http_code}' -X POST "$API" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "Content-Type: application/json" \
-d "$(jq -nc --arg u "$url" '{linkedin_url:$u, reveal_email:true}')")
case "$code" in
200) jq -c . body.json >> results.jsonl; return ;;
404) echo "$url" >> misses.txt; return ;;
401|402) echo "stop: $code" >&2; exit 1 ;;
429) wait=$(awk 'tolower($1)=="retry-after:"{print $2+0}' headers.txt)
sleep "${wait:-2}"; tries=$((tries+1)) ;;
*) echo "$code $url" >&2; return ;;
esac
done
echo "gave up: $url" >&2
}
while IFS= read -r url; do
[ -n "$url" ] && enrich "$url"
sleep 0.25
done < urls.txtSave it as enrich.sh and run bash enrich.sh. A 401 or 402 stops the run. Any other error, such as a 503, prints the status code and the URL to stderr and moves on.
Step 3: Stop on 402 and read the results
When the Free records are spent, the API answers 402 and the loop exits. Check how many you have left before a big run, with the free account call.
curl -s https://api.leadocean.io/v1/account -H "x-api-key: $LEADOCEAN_API_KEY"
wc -l results.jsonl misses.txt
jq -r '.data | [.profile_data.profile_full_name, .profile_data.profile_headline, .contact_data.email_status, .contact_data.contact_emails[0].email] | @csv' results.jsonl | headField names under profile_data, contact_data and resume_data are listed in the enrich docs. Since October 2026, resume_data carries past roles, education, certifications and awards when we hold them.
Step 4: Scale past 100 URLs
For lists above a few thousand, move to a paid plan and shorten the sleep: Solo allows 50 requests a second and 10k enrichments every 5 hours, Agency 200 a second and 1M a day. One loop sends one call at a time, so each call's round trip also sets its speed. The 429 handler covers you if you overshoot. Paid plans have a fair-use pace, not credits, so nothing stops when you reach it. The key is served at a reduced rate until the window resets, and meta.notice says which window.
If your list is a search result and not LinkedIn URLs, use person_id from search rows instead. About a fifth of people search can find have no LinkedIn URL.
Step 5: Clean the input first
Bad input wastes calls and time. Strip blank lines and tracking parameters, and drop duplicates before you run the loop.
sed 's/[?#].*$//; s#/*$##' urls.txt | grep -i 'linkedin.com/in/' | sort -u > clean.txt
mv clean.txt urls.txtKeep the miss file. A 404 means we hold nothing for that profile today. The dataset is refreshed monthly, so rerun misses.txt next month and some may now resolve, still at no cost if they miss again.
What you get
A successful call returns success, data and meta. This is the response shape from the API reference, with placeholder values:
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"has_email": true,
"email_status": "verified",
"contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
"contact_phones": [{ "phone": "+1 415 555 0133", "type": "mobile" }],
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme" }]
},
"resume_data": { "experiences": [], "educations": [] }
},
"meta": { "key_hash": "...", "fetched_at": "2026-10-01" }
}Contact points appear only when you send reveal_email. In results.jsonl you get one such object per line.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Re-export $LEADOCEAN_API_KEY and check the header name |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Honour Retry-After, or lengthen the sleep |
| 402 | The plan's records are spent | Check GET /v1/account, then upgrade on /pricing |
| 404 | We hold nothing for that profile | Costs nothing. It lands in misses.txt |
| 400 on the body | More than one identifier, or none | Send exactly one of linkedin_url, email, phone, person_id |
FAQ
Does a 404 cost a record?
No. A person we hold nothing for is a 404 and is free. Only profiles we return count.
Can I send many URLs in one request?
Enrich takes one person per call. For lists, loop as above. For an audience built from filters and not a URL list, use POST /v1/exports, which takes up to 50,000 rows a request.
Should I use POST or GET?
Both work on /v1/people/enrich. POST with a JSON body avoids URL-encoding the LinkedIn address.
What if my list has duplicate URLs?
Run sort -u urls.txt first. Each duplicate that returns a person is billed as a record.
Related
The same loop in other languages: Go, C# and TypeScript. More guides are in the LeadOcean blog. Plan limits are on /pricing.
Enrich your first 1,000 LinkedIn URLs free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →