You send a domain to POST /v1/companies/enrich and get one company record back. This page gives you a Python script that does that for a list of domains.
TL;DR
- It takes about 10 minutes and needs only the
requestspackage. - It spends one LeadOcean record per company returned. A 404 (domain not in our data) is free, so the Free plan's 1,000 records cover 1,000 hits.
- It retries on 429 using the
Retry-Afterheader and writes one JSON line per company.
Prerequisites
- Python 3.9 or newer and
pip install requests. - A LeadOcean API key, exported as
$LEADOCEAN_API_KEY. The Free plan gives one key, no card. - A file
domains.txtwith one domain per line, for exampleacme.com.
Step 1: Send one request
The endpoint takes a flat JSON body with domain or linkedin_url. Auth is the x-api-key header. Test it with curl before writing Python.
curl -s https://api.leadocean.io/v1/companies/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"acme.com"}'A hit returns 200 with a data object. A miss returns 404.
Step 2: Write the script
Save this as lookup.py. It reads domains.txt, calls the API once per domain and prints JSON Lines to stdout.
import json
import os
import sys
import time
import requests
URL = "https://api.leadocean.io/v1/companies/enrich"
HEADERS = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
def lookup(session, domain):
for _ in range(5):
res = session.post(URL, json={"domain": domain}, headers=HEADERS, timeout=30)
if res.status_code == 200:
return res.json()["data"]
if res.status_code == 404:
return None
if res.status_code == 429:
time.sleep(max(int(res.headers.get("Retry-After", 1)), 1))
continue
sys.exit(f"{domain}: HTTP {res.status_code} {res.text}")
sys.exit(f"{domain}: still rate limited")
def main():
with open("domains.txt") as f:
domains = [line.strip() for line in f if line.strip()]
with requests.Session() as session:
for domain in domains:
company = lookup(session, domain)
if company is not None:
print(json.dumps(company))
if __name__ == "__main__":
main()The 429 branch waits for the number of seconds in Retry-After, then tries again, up to five times. A 429 costs no records. Any other error stops the run and prints the status and body.
Three choices in the script matter. It reuses one requests.Session, so the TLS connection stays open between calls. It treats 404 as a normal answer, not an error. It exits on any other status, so a bad key or an empty balance stops the run on the first domain instead of burning through the list.
You can also call the same endpoint with GET and domain as a query parameter. The POST form is easier to extend, because the body is plain JSON and requests encodes it for you.
Step 3: Run it and save the results
printf 'acme.com\nexample.org\n' > domains.txt
python3 lookup.py > companies.jsonl
wc -l companies.jsonlEach line is one company, ready to load with json.loads or to import into a database or a spreadsheet tool. Misses are skipped, so the line count can be lower than the domain count. The loop is sequential on purpose. One call at a time rarely passes the Free plan's 5 requests a second, and the 429 branch covers the rest.
Step 4: Page through a search when you do not have the domains yet
Enrich returns one company per call, so there is nothing to page. To build the domain list first, call POST /v1/companies/search. The body is flat JSON, filters are arrays, limit runs 1 to 100, and you pass meta.nextCursor from the last page as cursor.
def search_pages(session, body):
while True:
res = session.post("https://api.leadocean.io/v1/companies/search",
json=body, headers=HEADERS, timeout=30)
res.raise_for_status()
page = res.json()
yield from page["data"]
cursor = page["meta"].get("nextCursor")
if not cursor:
return
body = {**body, "cursor": cursor}Call it with for company in search_pages(session, {"country": ["US"], "limit": 100}): and collect the domains from each company.
One search pages to 10,000 rows. The last page has meta.depthCapped true and no nextCursor. Narrow the filters instead of walking further. To size the list first, call GET /v1/companies/count, which is free. Search spends one record per company returned, so check the count before you page. The free plan's 1,000 records go fast on a 100-row page, so test with limit set to 5 first.
If you only need a handful of fields, drop the rest in lookup before you print. That keeps the file small and does not change what you spend: the record is counted when the company is returned, not when you save it.
What you get
This is the response shape from the API reference, with placeholder values. A hit is wrapped as success, data and meta. The script prints the data object, which holds these groups.
{
"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.
To check your balance before a large run, call GET /v1/account and GET /v1/usage. Both are free. Run them first so a 402 does not surprise you halfway through a list.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check that $LEADOCEAN_API_KEY is exported in the same shell as python3 |
| 429 | Requests above the plan's rate (5 a second on Free, 200 on Agency) | Keep the Retry-After sleep, or upgrade the plan |
| 402 | The 1,000 Free records are spent | Add a paid plan at leadocean.io/pricing. Solo is $97 a month |
| 404 | We hold nothing for that domain | Expected and free. The script skips it |
Empty companies.jsonl | domains.txt has the wrong path or only misses | Run from the folder that holds domains.txt. Use a bare domain, not a URL |
| Search stops at 10,000 rows | The depth cap on one search | Split the query with narrower filters |
FAQ
Do I need an SDK?
No. The API is plain HTTPS and JSON, so requests is enough. The standard library's urllib works too.
Can I look up by LinkedIn URL instead?
Yes. Send linkedin_url in place of domain in the same body.
How do I go faster than one request at a time?
Use a ThreadPoolExecutor with a worker count that keeps you under your plan's rate: 5 a second on Free, 50 on Solo, 200 on Agency. Keep the Retry-After handling.
Does a miss cost a record?
No. A 404 is free. A record is spent only for a company you receive.
Where do I see the other languages?
Same recipe in Go, C# and TypeScript. More guides are in the blog. Plan details are on pricing.
Run your first Python lookup on the free plan
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →