You enrich a list of LinkedIn URLs in Node.js by sending each one to POST /v1/people/enrich and appending the response to a file. The whole script is under 60 lines, uses the built-in fetch and needs no npm packages.
TL;DR
- Time: about 10 minutes to set up. Run time is roughly one request per URL, held to your plan's rate limit (5 a second on Free, 200 on Agency).
- Cost: one LeadOcean record per URL that matches a person. A URL we hold nothing for returns 404 and spends nothing.
- Result: an
enriched.jsonlfile with one person per line, plus a running count of hits and misses.
Prerequisites
- Node.js 18 or later (
node --version). Earlier versions have no globalfetch. - A LeadOcean API key from the free plan, exported as
$LEADOCEAN_API_KEY. The free plan gives 1,000 records for life and no card. - A
urls.txtfile with one LinkedIn profile URL per line.
export LEADOCEAN_API_KEY="your-key-here"
mkdir enrich && cd enrich
printf "https://www.linkedin.com/in/jane-doe\n" > urls.txtStep 1: Set up the client and read the list
Read the key from the environment, set the x-api-key header once and load your URLs. The base URL is https://api.leadocean.io.
import { readFileSync, appendFileSync } from "node:fs";
import { setTimeout as sleep } from "node:timers/promises";
const key = process.env.LEADOCEAN_API_KEY;
if (!key) throw new Error("Set LEADOCEAN_API_KEY first.");
const urls = readFileSync("urls.txt", "utf8")
.split("\n").map((l) => l.trim()).filter(Boolean);
let hits = 0, misses = 0;Step 2: Write the enrich call with retries on 429
Enrich takes exactly one of linkedin_url, email, phone or person_id. Send reveal_email: true to get the contact points in the same call, at no extra record.
A 429 means you went above your plan's rate limit. Read the Retry-After header and wait that long. If it is missing, back off 1, 2, 4 seconds.
async function enrich(url) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch("https://api.leadocean.io/v1/people/enrich", {
method: "POST",
headers: { "x-api-key": key, "content-type": "application/json" },
body: JSON.stringify({ linkedin_url: url, reveal_email: true }),
});
if (res.status === 404) return null;
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
await sleep(wait * 1000);
continue;
}
if (!res.ok) throw new Error(`${res.status} on ${url}`);
return (await res.json()).data;
}
throw new Error(`Gave up after 5 tries: ${url}`);
}A 404 is a normal miss, not an error. 401 and 402 are not retried: the thrown error stops the run so you see them.
Step 3: Page through the list and write results
Process the list in pages of 100. After each page, the file already holds every hit, so a crash loses at most the page in flight. Append each person as one JSON line.
for (let i = 0; i < urls.length; i += 100) {
for (const url of urls.slice(i, i + 100)) {
const person = await enrich(url);
if (!person) { misses++; continue; }
hits++;
appendFileSync("enriched.jsonl", JSON.stringify(person) + "\n");
}
console.log(`${hits} found, ${misses} not found`);
}Save the three blocks as one file named enrich.mjs and run node enrich.mjs. The loop is sequential. If answers come back faster than 5 a second on Free, the 429 handler waits and retries. To go faster on Agency (200 a second), run several enrich calls at once with Promise.all on slices of 20, and keep the 429 handler.
Step 4: Check a sample and pull the fields you need
Run 20 URLs before the full list. Then flatten the JSON lines into a CSV with the fields you plan to use. Field names follow the enrich docs.
jq -r '[.profile_data.profile_full_name, .contact_data.email_status,
.contact_data.contact_emails[0].email,
.contact_data.contact_current_experiences[0].company_name] | @csv' \
enriched.jsonl > people.csvA low hit rate usually means bad input. Strip ?utm_ parameters and trailing slashes, and keep only linkedin.com/in/ profile URLs. Check what you have spent with GET /v1/account, which costs nothing.
What you get
Each line of enriched.jsonl is one data object. This is the response shape from the API reference (openapi.json, checked September 2026), with placeholder values:
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_url": "https://www.linkedin.com/in/jane-doe" },
"contact_data": {
"contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
"contact_phones": [{ "phone": "+1 415 555 0133", "type": "mobile" }],
"has_email": true,
"has_phone": true,
"email_status": "verified",
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }]
},
"resume_data": { "experiences": [], "educations": [] }
}Read email_status before you send anything. LeadOcean does not refund bounces. resume_data carries past roles, education and certifications when we hold them; past employers have no company_id yet.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in the shell that runs node enrich.mjs. |
| 429 | Requests above the plan's rate limit (5 a second on Free, 200 on Agency) | Keep the Retry-After wait in Step 2. Lower your concurrency. |
| 402 | Out of records on the Free plan | Upgrade, or enrich fewer URLs. The 1,000 Free records are one-off. |
| 404 | We hold nothing for that URL | Not an error and it costs nothing. The script counts it as a miss. |
| Every call is a miss | URL is a company page, a search link or has a typo | Use a profile URL of the form linkedin.com/in/.... |
fetch is not defined | Node.js older than 18 | Upgrade Node.js, or run with a current LTS version. |
FAQ
Do I need axios or node-fetch?
No. Node.js 18 and later ship fetch, and the script uses only built-in modules. Add a queue library such as p-limit only if you want fixed concurrency on a paid plan.
How many records will 500 URLs use?
At most 500, one per matched person. Misses are free, so the real number is your match rate times 500. The free plan covers 1,000 in total.
Can I enrich by email instead of LinkedIn URL?
Yes. Replace linkedin_url with email in the request body. Send exactly one identifier per call.
What if I have thousands of URLs?
Run it on Solo ($97 a month, 50 requests a second, 10k enrichments every 5 hours) or Agency ($497 a month, 200 a second, 1M enrichments a day). Paid plans have a fair-use pace, not credits. Reaching it slows the key until the window resets; nothing stops. See pricing.
Are there versions in other languages?
Yes: Go, C# and TypeScript. More recipes are in the blog.
Enrich your first LinkedIn URLs from Node.js today
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →