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 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
- Node.js 20 or later (
node --version). Built-infetchmeans no npm packages. - 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 folder
mkdir domain-lookup && cd domain-lookup
export LEADOCEAN_API_KEY="your key from app.leadocean.io"
printf 'acme.com\nexample.org\n' > domains.txtCreate lookup.mjs in this folder. The .mjs extension lets you use import and top-level await.
Step 2: Write the lookup, retry and output loop
This sends one request per domain, waits out a 429, skips a 404 and appends each hit to companies.jsonl.
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");
const domains = readFileSync("domains.txt", "utf8")
.split("\n").map((d) => d.trim()).filter(Boolean);
async function lookup(domain) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch("https://api.leadocean.io/v1/companies/enrich", {
method: "POST",
headers: { "x-api-key": key, "content-type": "application/json" },
body: JSON.stringify({ domain }),
});
if (res.status === 429) {
const wait = Number(res.headers.get("retry-after")) || 2 ** attempt;
await sleep(wait * 1000);
continue;
}
if (res.status === 404) return null;
if (!res.ok) throw new Error(`${domain}: HTTP ${res.status}`);
return res.json();
}
throw new Error(`${domain}: still rate limited after 5 tries`);
}
for (const domain of domains) {
const result = await lookup(domain);
if (result) appendFileSync("companies.jsonl", JSON.stringify(result) + "\n");
console.log(`${domain}: ${result ? "ok" : "not found"}`);
await sleep(220);
}Run it with node lookup.mjs. Each result is written as soon as it arrives, so a crash loses nothing already fetched.
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, shorten the pause or run lookups in small parallel groups with Promise.all. 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. Parse it line by line:
import { readFileSync } from "node:fs";
for (const line of readFileSync("companies.jsonl", "utf8").split("\n").filter(Boolean)) {
const { data } = JSON.parse(line);
console.log(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 node lookup.mjs. |
| 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. 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. To find companies you do not know yet, use /v1/companies/search: 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 →