You can enrich a list of LinkedIn URLs in PHP with one file and the built-in cURL extension: read the URLs, call POST /v1/people/enrich for each, wait out any 429, and write the answers to a CSV. This guide gives you that file and explains each part.
TL;DR
- One PHP file, 39 lines, no Composer packages. It reads
urls.txtand writespeople.csv. - Time: about 10 minutes to set up. At 4 requests a second, 1,000 URLs take at least 4 minutes.
- Cost: 1 LeadOcean record per URL we hold a match for. A miss is a 404 and is free, so the Free plan's 1,000 records cover up to 1,000 matches.
Prerequisites
- PHP 8.0 or newer with the cURL extension (
php -m | grep curl). - A LeadOcean API key, exported as
$LEADOCEAN_API_KEY. The Free plan gives you one with 1,000 records for life, no card. - A
urls.txtfile with one LinkedIn profile URL per line.
Step 1: Get a key and prepare the list
Sign up at app.leadocean.io, create a key and put it in your shell. Never write the key into the source file.
export LEADOCEAN_API_KEY="your-key-here"
printf '%s\n' \
"https://www.linkedin.com/in/jane-doe-acme" \
"https://www.linkedin.com/in/john-roe-acme" > urls.txtThe enrich endpoint takes exactly one identifier per call: linkedin_url, email, phone or person_id. Enrich does not page. You make one call per URL, so "paging" here means walking your file line by line.
Step 2: Write the PHP script
Save this as enrich.php. Requests go to https://api.leadocean.io with the x-api-key header.
<?php
function enrich(string $key, string $url): array {
$body = json_encode(['linkedin_url' => $url, 'reveal_email' => true]);
for ($try = 0; $try < 5; $try++) {
$retryAfter = 1;
$ch = curl_init('https://api.leadocean.io/v1/people/enrich');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['x-api-key: ' . $key, 'Content-Type: application/json'],
CURLOPT_HEADERFUNCTION => function ($c, $h) use (&$retryAfter) {
if (stripos($h, 'retry-after:') === 0) {
$retryAfter = max(1, (int) trim(substr($h, 12)));
}
return strlen($h);
},
]);
$data = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($data === false) { sleep(1); continue; }
if ($status === 429) { sleep($retryAfter); continue; }
return [$status, $data];
}
return [0, 'gave up after 5 tries'];
}
$key = getenv('LEADOCEAN_API_KEY') ?: exit("Set LEADOCEAN_API_KEY\n");
$out = fopen('people.csv', 'w');
fputcsv($out, ['linkedin_url', 'status', 'json']);
foreach (file('urls.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
$url = trim($line);
[$status, $data] = enrich($key, $url);
fputcsv($out, [$url, $status, $data]);
usleep(250000);
}
fclose($out);Three choices in that file matter:
reveal_email: truereturns the contact points on the record. It adds no extra record.usleep(250000)pauses 250 ms between calls. That keeps you at 4 requests a second, under the Free plan's limit of 5.- On a 429 the header callback reads
Retry-After, the script sleeps that many seconds, and tries again, up to 5 times. See API rate limits and 429 errors.
Step 3: Run it
php enrich.php
head -3 people.csvThe status column tells you what happened per URL: 200 is a match, 404 is nobody found. Filter on it before you load rows anywhere else.
Step 4: Parse the fields you need
The CSV stores the raw JSON so you lose nothing. When you know which fields you want, run json_decode($data, true) on each row. The current role is in contact_current_experiences[] and phones are in contact_phones[].phone. Since October 2026 the same call also returns career history in resume_data: past roles, education, certifications and awards.
Past employers carry no company_id yet. If you need a company record, call POST /v1/companies/enrich with the domain.
What you get
The response shape from the API reference, with placeholder values. This is not a real person.
{
"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.doe@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": [], "certifications": [] }
},
"meta": { "key_hash": "...", "fetched_at": "2026-10-01" }
}Source: response shape from the API reference (openapi.json, October 2026), with field names from the enrich docs. Each record also carries a fetched_at date, and the dataset refreshes monthly.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key, or $LEADOCEAN_API_KEY is empty in this shell | Re-export the key and check with echo ${#LEADOCEAN_API_KEY} |
| 429 | You sent faster than the plan allows: 5 a second on Free, 200 on Agency | Raise the usleep value, or rely on the Retry-After retry in the code |
| 402 | The records are spent. Free is 1,000 for life | Stop the run and upgrade. Paid plans slow at their pace instead of returning 402 |
| 404 | No person matched that URL. It is free | Keep the row, mark it unmatched, move on |
| 400 on a row | Not exactly one identifier, or a malformed URL | Send only linkedin_url, and trim spaces and trailing blank lines |
FAQ
Can I run the calls in parallel in PHP?
Yes, with curl_multi_init. Keep the total under your plan's per-account limit: 5 requests a second on Free, 50 on Solo, 200 on Agency. A single sequential loop is simpler, and on Free it is all the speed you can use.
Does a URL with no match use a record?
No. A person we hold nothing for is a 404 and costs nothing. Only a match returns a record.
Should I use GET or POST?
Both work on /v1/people/enrich. POST with a JSON body avoids URL-encoding the LinkedIn address.
What if I have tens of thousands of URLs?
Run the same loop in batches and write each batch to its own file. Solo allows 10k enrichments every 5 hours and Agency 1M a day. Paid plans have a fair-use pace instead of credits, and reaching it slows the key rather than stopping it.
Is there a bulk option?
Not for a list of URLs. Enrich is one person per call. POST /v1/exports exports an audience built from search filters, up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free; it starts on Solo at $97 a month.
See the same recipe in Go, C# and TypeScript. The rest of the series is in the blog hub.
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 →