TL;DR
- Send
POST /v1/companies/enrichwith{"domain": "acme.com"}and thex-api-keyheader. You get one company back. - Setup takes about 10 minutes. A run of 100 domains spends at most 100 of your 1,000 free records.
- The script retries on 429 using
Retry-After, reads domains from a file in batches and writes one JSON line per company.
The lookup is a single call. The rest of this guide is the loop around it: pacing, retries and output.
Prerequisites
- PHP 8.0 or newer with the cURL extension (
php -m | grep curl). - A LeadOcean API key from app.leadocean.io. Free needs no card.
- The key in your shell as
$LEADOCEAN_API_KEY. - A file
domains.txtwith one domain per line, such asacme.com.
Step 1: Set up the files
export LEADOCEAN_API_KEY="your key from app.leadocean.io"
printf 'acme.com\nexample.org\n' > domains.txtNo Composer packages are needed. The cURL extension and json_encode ship with PHP.
Step 2: Write the lookup, retry and output loop
Save this as lookup.php. It sends one request per domain, waits out a 429, skips a 404 and appends each hit to companies.jsonl.
<?php
$key = getenv('LEADOCEAN_API_KEY') ?: exit("Set LEADOCEAN_API_KEY\n");
$domains = array_filter(array_map('trim', file('domains.txt')));
$out = fopen('companies.jsonl', 'a');
function lookup(string $domain, string $key): ?string {
for ($attempt = 0; $attempt < 5; $attempt++) {
$retryAfter = null;
$ch = curl_init('https://api.leadocean.io/v1/companies/enrich');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['domain' => $domain]),
CURLOPT_HTTPHEADER => ['x-api-key: ' . $key, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADERFUNCTION => function ($c, $h) use (&$retryAfter) {
if (stripos($h, 'retry-after:') === 0) $retryAfter = (int) trim(substr($h, 12));
return strlen($h);
},
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 429) { sleep($retryAfter ?: 1 << $attempt); continue; }
if ($status === 404) return null;
if ($status !== 200) throw new RuntimeException("$domain: HTTP $status");
return $body;
}
throw new RuntimeException("$domain: still rate limited after 5 tries");
}
foreach (array_chunk($domains, 50) as $batch) {
foreach ($batch as $domain) {
$json = lookup($domain, $key);
if ($json === null) { echo "$domain: not found\n"; }
else { fwrite($out, trim($json) . "\n"); echo "$domain: ok\n"; }
usleep(220000);
}
fflush($out);
}Run it with php lookup.php. Each batch of 50 is flushed to disk, so a crash loses at most one batch.
Step 3: Match the pace to your plan
The 220 ms pause keeps you near 4.5 requests a second, under the 5 a second limit on Free. Rate limits are per key and per plan:
| Plan | Requests a second |
|---|---|
| Free | 5 |
| Solo | 50 |
| Agency | 200 |
| Platform | 10,000 |
On a paid plan, lower the usleep value or run several processes over different slices of the file. The retry branch still protects you if you overshoot. See pricing for what each plan includes.
Step 4: Read the output
Each line in companies.jsonl is one JSON response. Decode it line by line:
foreach (file('companies.jsonl') as $line) {
$row = json_decode($line, true);
print_r($row['data']['company_data']);
}The company fields sit under company_data. Detected technologies and metrics have their own objects.
What you get
Each response has this shape, from the API reference (placeholder values, not a real record):
{
"success": true,
"data": {
"company_data": {
"company_name": "Acme",
"company_domain": "acme.com",
"company_type": "Privately Held",
"company_industries": [{ "name2": "Software Development", "priority": 1 }],
"company_employees": { "number_of_employees": 120, "number_of_employees_code": "51-200" },
"company_primary_address": { "city": "San Francisco", "country_code": "US" },
"company_number_of_detected_technologies": 2
},
"company_detected_technologies": [
{ "technology_product": { "name": "Example CDN" }, "technology_category": { "name2": "Content Delivery Network" } }
],
"company_metrics": { "completion_score": 90, "marketability_score": 80 },
"meta": { "sources": ["own"], "schema": "leadocean.company.v1" }
},
"meta": { "source": "leadocean" }
}Response shape from the company lookup docs (October 2026), placeholder values. Fields we hold nothing for come back as null. company_data also carries funding, revenue, NAICS and SIC codes, and company_number_of_profiles when we hold them.
The console prints one line per domain: acme.com: ok or example.org: not found.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in the same shell as php lookup.php. |
| 429 | Requests above your plan's rate limit (5 a second on Free, 200 on Agency) | The loop waits Retry-After. Raise the pause or move up a plan. |
| 402 | Records spent | Free is 1,000 records for life. Move to a paid plan or trim the list. |
| 404 | No company held for that domain | Costs nothing. Check the spelling and use the bare domain, such as acme.com. |
Empty companies.jsonl | Every domain returned 404, or the file had blank lines only | Read the console output and test one known domain by hand. |
FAQ
Can I use GET instead of POST?
Yes. GET /v1/companies/enrich?domain=acme.com returns the same record and costs the same one record.
Can I look up by LinkedIn URL instead?
Yes. The body takes domain or linkedin_url.
How do I page through results?
Enrichment returns one company per call, so you page your own domain list, as array_chunk($domains, 50) does. To find companies you do not know yet, use /v1/companies/search. It takes a flat JSON body, limit from 1 to 100 and a cursor.
Does a failed lookup cost a record?
No. A 404 is free. Only a company you receive counts as one record.
Where are the same recipes in other languages?
See Go, C# and TypeScript.
Look up your first 100 companies by domain, free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →