You get an email from a LinkedIn URL by sending the URL to an enrichment API and asking it to reveal contact points. With LeadOcean that is one POST to /v1/people/enrich with linkedin_url and reveal_email: true.
This guide is the short version. No browser extension, no copy and paste. Real requests, the response shape, and the errors you will hit.
TL;DR
- One call to
POST /v1/people/enrichturns a LinkedIn URL into a work email plus itsemail_status. It takes about 10 minutes the first time, seconds after that. - Cost: 1 record per profile we hold. A 404 (nobody matched) costs nothing. The Free plan gives you 1,000 records for life, no card.
- Check
email_statusbefore you send. There is no refund for bounces.
Prerequisites
- A LeadOcean API key from app.leadocean.io. Free works, and it carries both the search and enrich scopes.
- The key in your shell as
$LEADOCEAN_API_KEY. curlandjq. Python or Node works the same way.- One or more LinkedIn profile URLs. Examples below use a placeholder:
https://www.linkedin.com/in/jane-doe-example.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Check your account and balance
Confirm the key works and see how many records you have left. GET /v1/account is free.
curl -s https://api.leadocean.io/v1/account \
-H "x-api-key: $LEADOCEAN_API_KEY" | jqA 401 here means the key is wrong or missing. Fix that before anything else.
Step 2: Enrich one LinkedIn URL
Send exactly one identifier. For this task it is linkedin_url. Set reveal_email to true, because the default for a LinkedIn lookup is false and you would get the profile without the address.
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",
"reveal_email": true
}' | jq '.data.contact_data'reveal_email adds every email and phone we hold for that person at no extra record. You pay one record for the person, not one per field.
Step 3: Read the status before you use the address
The contact_emails array comes back with a status on each address. Filter to what is worth sending to.
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","reveal_email":true}' \
| jq '.data.contact_data.contact_emails[]
| select(.status=="verified" or .status=="catch_all_valid")
| .email'verified and catch_all_valid are safe defaults. catch_all is a gamble on a domain that accepts everything. risky, unknown, invalid, role, disposable and spam_trap should not go into a sequence.
Look at type too. work is the current employer. work_other is a former employer. personal is a personal address, and our verifier has not tested those.
Step 4: Loop over a list of URLs
For a list, run the same call per row and skip the misses. A 404 means we hold nothing for that profile, so it is free.
while read -r url; do
out=$(curl -s -w "\n%{http_code}" -X POST https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"linkedin_url\":\"$url\",\"reveal_email\":true}")
code=${out##*$'\n'}
body=${out%$'\n'*}
if [ "$code" = "200" ]; then
echo "$url,$(echo "$body" | jq -r '.data.contact_data.contact_emails[0].email')"
else
echo "$url,,$code"
fi
sleep 0.25
done < urls.txt > emails.csvThe sleep 0.25 keeps you at about 4 requests a second, under the 5 a second Free limit. On Agency the limit is 200 a second. Above 1,000 rows, use POST /v1/exports instead, which takes up to 50,000 rows a request.
Step 5: Script it in Python
The same call from code. This version writes one row per URL and keeps the status next to the address, so the filter in Step 3 happens before anything reaches a sequencer.
import os, time, csv, requests
API = "https://api.leadocean.io/v1/people/enrich"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def lookup(url):
r = requests.post(API, headers=HEADERS,
json={"linkedin_url": url, "reveal_email": True})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 1)))
return lookup(url)
if r.status_code != 200:
return None
emails = r.json()["data"]["contact_data"].get("contact_emails", [])
good = [e for e in emails if e["status"] in ("verified", "catch_all_valid")]
return good[0] if good else None
with open("urls.txt") as f, open("emails.csv", "w", newline="") as out:
w = csv.writer(out)
for url in (line.strip() for line in f if line.strip()):
hit = lookup(url)
w.writerow([url, hit["email"] if hit else "", hit["status"] if hit else ""])
time.sleep(0.25)Two details matter. A 429 is not charged, so retrying after Retry-After is safe. And a profile that returns only a catch_all address is skipped on purpose: decide that one by hand.
Step 6: Do it in Claude with the MCP server
If you work in Claude, the hosted MCP server at https://api.leadocean.io/mcp signs in with OAuth, so no key is pasted. It exposes 13 tools. leadocean_get_lead looks up one person by LinkedIn URL, work email, phone or person_id, and costs 1 record. A lookup that finds nobody costs nothing.
Use leadocean_get_lead for https://www.linkedin.com/in/jane-doe-example.
Reveal the work email. Tell me its email_status and whether the address
is work or work_other. Do not guess if nothing comes back.To size a list before you spend anything, call leadocean_count_leads. It is free.
What you get
Response shape from the API reference, with placeholder values (not a real person):
{
"success": true,
"data": {
"profile_data": { "...": "name, headline, location" },
"contact_data": {
"has_email": true,
"has_phone": true,
"email_status": "verified",
"email_type": "work",
"contact_emails": [
{
"email": "jane.doe@acme.com",
"type": "work",
"status": "verified",
"verified_batch_date": "2026-09-20"
}
],
"contact_phones": [
{ "phone": "+14155550133", "type": "mobile", "priority": 1 }
],
"contact_current_experiences": [ { "...": "current role" } ]
},
"resume_data": { "...": "past roles, educations, certifications" }
},
"meta": { "source": "leadocean" }
}Since 17 October 2026 the same call also returns resume_data: past roles, education, certifications and awards. Past employers carry no company_id yet.
For scale, here is a free count run on 21 October 2026 with leadocean_count_leads, filtered to jobLevel VP and country US. It counted only addresses worth sending to (verified, catch_all_valid or catch_all):
{ "total": 1242801, "totalIsExact": true, "totalAll": 3102417, "distinctCompanies": 226527, "credits": 0 }That is the count, not a spend. Counts cost 0 records.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Re-export $LEADOCEAN_API_KEY and send it as x-api-key. |
| 404 | We hold nothing for that profile | Free. Try the person's email, or search by company domain and enrich by person_id. |
| 402 | Out of records (Free is 1,000 for life) | Check GET /v1/account. Move to Solo at $97 a month, or Agency at $497 a month. |
| 429 | Above your plan's rate limit (5 a second on Free, 200 on Agency) | Honour the Retry-After header and slow the loop. |
Empty contact_emails | You left out reveal_email: true, or we hold no address | Set reveal_email to true. Check has_email in the response. |
| Export refused | Over 50,000 rows a request or 500,000 rows a day | Split the file. Bulk CSV export is not on Free. |
FAQ
Can I get an email from a LinkedIn URL for free?
Yes, up to 1,000 records for life on the Free plan, no card. Each matched profile spends one record. A profile with no match spends none.
Does LeadOcean scrape LinkedIn to do this?
No page is fetched when you call the API. You send the URL as a lookup key, and the answer comes from the LeadOcean dataset (693M people, 366M with an email as of September 2026). The dataset is refreshed monthly, and each record carries its own fetched_at date.
What if the profile has no LinkedIn URL on file?
About a fifth of people that search can find have no LinkedIn URL. Search by company domain or title first, then enrich by person_id from meta.source_ids.person_id.
How do I go the other way, from an email to a profile?
Send email instead of linkedin_url to the same endpoint. See searching LinkedIn by email.
Which email should I send to?
Send to verified or catch_all_valid work addresses. Read how to find someone's email from LinkedIn and the shorter overview for the manual options. Pricing for volume is on /pricing.
Turn LinkedIn URLs into verified emails
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →