TL;DR
- Send
POST /v1/companies/enrichwith{"domain": "acme.com"}and anx-api-keyheader. A match costs 1 record. A 404 costs nothing. - Add
--retry 5and curl waits out a 429 for you. A 429 is free. - Setup takes about 10 minutes. Twenty domains spend at most 20 records, which fits the Free plan's 1,000.
Prerequisites
- A LeadOcean API key in
$LEADOCEAN_API_KEY. Get one free at app.leadocean.io. No card. curl7.66 or newer (for Retry-After handling) andjq.- A file
domains.txtwith one domain per line, such asacme.com.
Step 1: Look up one domain
One call returns one company. The body is flat JSON. You can send linkedin_url instead of domain.
curl -s -X POST https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"domain": "acme.com"}' | jq .GET /v1/companies/enrich?domain=acme.com also works. See the company lookup docs (October 2026).
Step 2: Retry on 429
Above your plan's rate limit the API answers 429 with a Retry-After header. The limit is 5 requests a second on Free, 50 on Solo and 200 on Agency. A 429 costs no records.
With --retry, curl treats a 429 as a transient error, retries it and honors Retry-After (curl 7.66 or newer). See the curl manual (September 2026).
curl -s --retry 5 -X POST https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"domain": "acme.com"}'Step 3: Loop over a domain list
This script reads domains.txt, writes one JSON object per line and logs misses to stderr. It stops on any status it does not expect, so a bad key or a spent plan does not burn the whole file.
set -u
tmp=$(mktemp)
while read -r d; do
[ -z "$d" ] && continue
code=$(curl -s -o "$tmp" -w '%{http_code}' --retry 5 \
-X POST https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d "$(jq -nc --arg d "$d" '{domain:$d}')")
case $code in
200) jq -c --arg d "$d" '{domain:$d} + .data' "$tmp" ;;
404) echo "miss: $d" >&2 ;;
*) echo "stop: HTTP $code at $d" >&2; exit 1 ;;
esac
sleep 0.25
done < "$1"The sleep 0.25 keeps you near 4 requests a second, under the Free limit. Drop it on Solo or Agency. Save it as lookup.sh and run bash lookup.sh domains.txt > companies.ndjson. Misses print to your terminal and never reach the file.
Two details matter. The jq -nc --arg call builds the JSON body, so a domain with odd characters cannot break the request. And -o "$tmp" -w '%{http_code}' splits the body from the status, which lets the case block treat a 404 as a miss instead of an error.
Step 4: Page through the people at a company
Company enrich returns the company only. To list its people, call GET /v1/companies/{domain}/people and follow meta.nextCursor. Each person returned costs 1 record, so keep limit small while you test.
cursor=""
while :; do
page=$(curl -s -G "https://api.leadocean.io/v1/companies/acme.com/people" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
--data-urlencode "limit=25" --data-urlencode "cursor=$cursor")
echo "$page" | jq -c '.data[]' >> people.ndjson
cursor=$(echo "$page" | jq -r '.meta.nextCursor // empty')
[ -z "$cursor" ] && break
donePass the cursor back unchanged. A hand-built or edited cursor gets a 400. A roster stops at 10,000 rows, and the last page then carries meta.depthCapped: true.
Step 5: Resume and dedupe a long run
A 404 costs nothing but a matched domain costs 1 record. Do not pay twice for a domain after a crash. Dedupe the input and skip anything already in the output file.
sort -u domains.txt > todo.txt
jq -r '.domain' companies.ndjson 2>/dev/null | sort -u > done.txt
comm -23 todo.txt done.txt > remaining.txt
bash lookup.sh remaining.txt >> companies.ndjsonMisses are not in done.txt, so a rerun asks about them again. They are free, so that is fine.
Step 6: Check what you spent
Sizing and account calls are free. Read your balance before and after a run with GET /v1/account. The difference is the records you spent.
curl -s https://api.leadocean.io/v1/account -H "x-api-key: $LEADOCEAN_API_KEY" | jq .On Free the 1,000 records are one-off. On paid plans there are no credits. Each plan has a fair-use pace, and reaching it slows your key until the window resets. Nothing stops and nothing needs a top-up. See pricing for Solo at $97 a month and Agency at $497 a month.
What you get
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.
{
"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" }
}Step 3 prepends "domain": "acme.com" to data so each line of companies.ndjson stands alone. Read it back with jq -r '.domain' companies.ndjson.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check echo ${#LEADOCEAN_API_KEY} is not 0. Copy the key again from the app. |
| 429 | Above the plan's rate limit (5 a second on Free, 200 on Agency) | Keep --retry 5 and the sleep. The response has a Retry-After header. A 429 is free. |
| 402 | The plan's records are spent (Free is 1,000 for life) | Check GET /v1/account, then move to Solo or Agency. The script stops here. |
| 404 | No company held for that domain | Check the domain: acme.com, not https://www.acme.com. A 404 costs nothing. |
| 400 "Invalid cursor" | The cursor was edited, or minted before 2026-09-19 | Restart the roster walk with no cursor. |
meta.depthCapped is true | A roster reached 10,000 rows | Narrow the search instead of paging harder. |
FAQ
How many records does a company lookup cost?
One per company returned. A miss, a 404 and a 429 cost nothing. Twenty domains cost at most 20 records.
Can I look up by LinkedIn URL instead?
Yes. Send {"linkedin_url": "..."} in the same body. Use exactly the field you have.
How do I send many domains faster?
Raise the rate with a plan that allows it: 50 requests a second on Solo, 200 on Agency. Run several copies of the script on separate files. Stay below your limit or the retries slow you down.
Where do I find the same lookup in another language?
See the Go version, the C# version and the TypeScript version. Plans are on pricing.
More recipes live in the blog hub.
Look up your first 1,000 company domains free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →