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

How-to guide

How to Enrich a List of LinkedIn URLs (cURL and Bash)

A short bash loop that sends each LinkedIn URL to POST /v1/people/enrich, retries on 429 and writes one JSON line per person.

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

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 separate misses.txt for 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.
  • curl and jq.
  • A file urls.txt with 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.

bash
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.

bash
#!/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.txt

Save 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.

bash
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 | head

Field 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.

bash
sed 's/[?#].*$//; s#/*$##' urls.txt | grep -i 'linkedin.com/in/' | sort -u > clean.txt
mv clean.txt urls.txt

Keep 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:

json
{
  "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

ErrorCauseFix
401Missing or wrong x-api-keyRe-export $LEADOCEAN_API_KEY and check the header name
429Above the plan's rate limit (5 a second on Free, 200 on Agency)Honour Retry-After, or lengthen the sleep
402The plan's records are spentCheck GET /v1/account, then upgrade on /pricing
404We hold nothing for that profileCosts nothing. It lands in misses.txt
400 on the bodyMore than one identifier, or noneSend 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.

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 →