A company research API takes what you already know about a company, usually its name or website, researches it when you ask and returns the fields you define, with the sources behind them. A company information API that looks a record up in a stored database answers a different question, and both have their place. Sixtyfour is a company research API: you send a company name or website and a struct describing the fields you want, and its Company Intelligence endpoint returns those fields with the pages it read them on and, if you ask, a 0 to 100 confidence score and a justification for each one.
Below is how we build on it: one company synchronously, then asynchronously, then a list in one bulk job, then webhooks, followed by how to design fields and what to do with unsure answers. Request and response shapes follow our API docs, checked on 13 November 2026, and the companies in the examples are invented.
What is a company research API?
Searches for a company information API, a company search API and a company research API usually describe three different jobs:
- Finding companies that match a description, such as laboratory equipment makers in the UK with 50 to 500 staff. That is search, and it returns a list.
- Looking up the record a provider already holds for a company you name. That is a lookup against a stored database, usually quick and predictable in shape, and as current and complete as the record.
- Researching a company you already have, to answer questions specific to your workflow: who owns it, what it sells, who runs it and what it announced this year. That is research, and the work happens when you ask.
A lookup fits when the fields you need are the ones the database stores and you need them for a lot of rows quickly. Research fits when the fields are particular to your question, when the company is small or new and a stored record is thin, or when someone downstream needs to open the page a fact came from. Plenty of teams run both: a lookup for standard firmographics, and research for the questions a record does not answer.
Sixtyfour does the research job through Company Intelligence and builds lists through its search endpoints, which we come to below. A matching People Intelligence endpoint researches a person, so one API covers researching people and companies.
How to research a company with Sixtyfour
Every request goes to https://api.sixtyfour.ai with an API key in the x-api-key header, and keys are created in the web app under Settings, then API Keys. A Company Intelligence request has two required parts. target_company is what you already know: any of company_name, domain, website, linkedin, location and other keys. struct is what you want back: each key becomes a field in the response, and its value tells the research agent what to find.
Our running example is Wraxholm Instruments, an invented maker of laboratory calibration equipment at wraxholm.example (the .example name is reserved for documentation). We want six fields: what it sells, its headquarters, a headcount range, its parent company, its current leadership and its news from the past year.
import requestsAPI = "https://api.sixtyfour.ai"HEADERS = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}body = {"target_company": {"company_name": "Wraxholm Instruments","website": "https://wraxholm.example",},"struct": {"what_they_sell": "What the company sells and to which customers, in one sentence","headquarters": "City and country of the headquarters","headcount_range": {"description": "Number of employees","type": "enum","options": ["1-10", "11-50", "51-200", "201-1000", "1001+"],},"parent_company": "The company that owns this one, if any","leadership": {"description": "Current executives named on the company's own site or in its filings","type": "object_list","subfields": [{"key": "name", "type": "string", "description": "Full name"},{"key": "title", "type": "string", "description": "Current title"},],},"recent_news": {"description": "News about the company from the last 12 months, one line each with its month","type": "list[str]",},},"tier": "medium","field_confidence": True,}response = requests.post(f"{API}/company-intelligence", headers=HEADERS, json=body, timeout=900)response.raise_for_status()result = response.json()
The timeout is 900 seconds because a Company Intelligence run takes up to 10 minutes, and our docs ask for sync client timeouts of at least 15 minutes. The response below is illustrative. It follows the documented shape, trimmed to four of the six fields, without org_chart and the deprecated findings, and with confidence shown for three fields:
{"structured_data": {"what_they_sell": "Calibration instruments for laboratory balances, sold to university and hospital laboratories","headquarters": "Leeds, United Kingdom","headcount_range": "51-200","parent_company": "Welbrae Holdings","company_name": "Wraxholm Instruments","website": "https://wraxholm.example"},"notes": "Found the company site, a registry filing and a 2024 press release. Headcount rests on one aggregator range.","references": {"https://wraxholm.example/about": "Products and customers","https://wraxholm.example/contact": "Headquarters address","https://registry.example/companies/wraxholm-instruments": "Registered address and corporate shareholder","https://press.example/2024/welbrae-acquires-wraxholm": "Acquisition by Welbrae Holdings"},"field_confidence": {"headquarters": {"confidence": 94,"justification": "The contact page and the registry filing give the same city.","sources": ["https://wraxholm.example/contact", "https://registry.example/companies/wraxholm-instruments"]},"headcount_range": {"confidence": 46,"justification": "A data aggregator's range, not confirmed elsewhere.","sources": []},"parent_company": {"confidence": 78,"justification": "A 2024 press release names Welbrae Holdings as the buyer, and the registry lists it as shareholder.","sources": ["https://press.example/2024/welbrae-acquires-wraxholm", "https://registry.example/companies/wraxholm-instruments"]}}}
Four parts matter. structured_data holds one key per struct field and echoes the keys you sent in target_company. references maps each source URL to what was found there, and notes is a short summary of the research. field_confidence mirrors the shape of your struct: each value gets a score from 0 to 100, a sentence of justification and the URLs it was read on, and each item in an object list is scored subfield by subfield. Without field confidence, the response carries one confidence_score from 0 to 10 for the whole record instead, and the two never appear together.
To get people as well, set find_people to true with a people_focus_prompt such as "The founder and the head of operations". The people found come back in structured_data.leads, and lead_struct chooses their fields in the same format as struct.
Tiers and cost
tier sets research depth. List prices as of November 2026, in credits per company, from our credits guide:
| Tier | Credits | What our docs say it is for |
|---|---|---|
micro | 0.3 | A lightweight lookup for very high volume, up to 10 struct fields |
low (default) | 1 | Fast, low-cost firmographic data |
medium | 5 | Deeper research: more sources, more fields filled |
high | 15 | Deep, recursive research built for high-stakes accounts and diligence, enabled on request |
Field confidence, find_people and full_org_chart cost nothing extra, and a single-company job that fails is not charged. Company Intelligence has no deeper tier, and for an in-depth investigation of a company our docs point to Atlas, which is enabled on request. We start new integrations at medium.
How do you run research that takes several minutes?
A sync call holds a connection open for up to 10 minutes, which is fine in a script and fragile in a service. Our docs are direct about what happens when it drops: "A sync call keeps running after your client times out or disconnects, and it is charged when it finishes." A retry then starts and charges a second run. When Sixtyfour is at capacity, a sync call returns 503 while an async job waits its turn. So in production we start the job and poll for it.
POST /company-intelligence-async takes the same body and answers at once with a task_id and a status of RUNNING. If any key in the body was not recognised, the start response lists it in unrecognized_fields, which catches a misspelt field_confidence before you wonder why no scores came back.
GET /job-status/{task_id} reports running until the job reaches a final status: completed, failed, cancelled, terminated or timed_out. This is the polling helper from our async jobs page, with its two-hour timeout written out in seconds:
import timedef wait_for_job(task_id, interval=10, timeout=7200):deadline = time.monotonic() + timeoutwhile time.monotonic() < deadline:response = requests.get(f"{API}/job-status/{task_id}", headers=HEADERS, timeout=30)if response.status_code in (429, 503):# Over the polling budget, or status briefly unavailable: the job keeps running.time.sleep(float(response.headers.get("Retry-After", interval)))continueresponse.raise_for_status()job = response.json()if job["status"] != "running":return jobtime.sleep(interval)raise TimeoutError(f"Job {task_id} still running after {timeout} seconds")start = requests.post(f"{API}/company-intelligence-async", headers=HEADERS, json=body, timeout=60)start.raise_for_status()task_id = start.json()["task_id"] # store it before anything elsejob = wait_for_job(task_id)if job["status"] != "completed":raise RuntimeError(f"Job {job['status']}: {job.get('error', 'no reason given')}")result = job["result"]print(job.get("charge_credits"))
Three rules come with it:
- Store the
task_idbefore anything else. Company Intelligence takes nodispatch_id, so in our docs' words "a repeated async start also runs and charges again". - Poll each job every 10 seconds or slower. Job status shares one budget per organisation, 300 reads a minute with a burst of 15, which our rate limits page says covers 50 jobs in flight at that pace. A
429on a status read does not affect the job: waitRetry-Afterseconds and read again. - Expect a final status other than
completed. A Company Intelligence job that does not start within 24 hours endsfailed, and a failed single-company job is not charged.
How do you find the companies to research?
Sixtyfour's search endpoints build the list. Filter Search, POST /search/query with "mode": "company", searches companies by structured fields such as location, headcount and industry, and takes a natural language query as an alternative to filters. Deep Search, POST /search/start-deep-search with "mode": "company" (its default is people), turns a description of up to 10,000 characters into a list as an async task, with up to five running per organisation and each running for at most 15 minutes. Lookalike Search finds people only, so for companies we start from one of the other two.
import csvpayload = {"mode": "company","query": "Laboratory calibration equipment makers in the United Kingdom with 50 to 500 employees","output_shape": "workflow","page_size": 100,"max_results": 200,}rows = []while True:response = requests.post(f"{API}/search/query", headers=HEADERS, json=payload, timeout=60)response.raise_for_status()page = response.json()rows += [(r["name"], r["website"]) for r in page["results"]]if not page["has_more"]:breakpayload = {"cursor": page["next_cursor"]}with open("companies.csv", "w", newline="") as f:writer = csv.writer(f)writer.writerow(["company_name", "website"])writer.writerows(rows)
Read two fields on the first page before you trust the list. parsed_query is the structured query your sentence was turned into, and unmet_constraints, when present, lists the parts of the query the search did not apply, so results can include rows outside them. With "output_shape": "workflow", each company row is flat, with columns such as name, website, domain and employeeCount. Filter Search costs 0.1 credits per result returned, as of November 2026.
How do you research a list of companies with an API?
For more than a handful of companies, we upload them as one Bulk Intelligence job instead of starting one async job per row. POST /bulk-intelligence/company takes multipart/form-data with two parts: file, a CSV, JSON, JSONL or NDJSON file of up to 100 MB and 100,000 rows, and config, a JSON string with the same options as the single-company request (struct, tier, field_confidence, research_plan, find_people and the rest) plus three bulk-only ones. Each row is the input. The docs do not fix the column names for a company file: the one sample, on the Start Bulk Company reference, has company_name and domain columns. We name ours after two target_company keys, company_name and website, and give full URLs, as the Company Intelligence example does. The file for our example:
company_name,websiteWraxholm Instruments,https://wraxholm.exampleWraybourne Logistics,https://wraybourne.exampleWarrowby Print,https://warrowby.exampleWixendale Ceramics,https://wixendale.example
import jsonconfig = {"struct": body["struct"],"tier": "medium","field_confidence": True,"result_formats": ["csv", "ndjson"],"webhook_url": "https://your-server.example/webhooks/sixtyfour",}with open("companies.csv", "rb") as f:submit = requests.post(f"{API}/bulk-intelligence/company",headers={"x-api-key": "YOUR_API_KEY", "Idempotency-Key": "wraxholm-list-2026-11-12"},files={"file": ("companies.csv", f, "text/csv")},data={"config": json.dumps(config)},timeout=120,)submit.raise_for_status()task_id = submit.json()["task_id"]print(submit.json().get("estimated_cost_credits"))
The response carries the task_id, a row_count and estimated_cost_credits. A submission your balance does not cover is refused with 402, and a bulk job costs the same per row as single requests. The Idempotency-Key header, up to 255 characters, makes the submission safe to retry: resubmitting with the same key within 24 hours returns the original job without the row count or cost estimate, hence the .get in the code. Submissions are limited to 10 a minute per organisation. Sixtyfour checks the webhook URL when you submit, so use your own receiver's.
While the job runs, job status carries progress with completed and total row counts. A completed bulk job has no inline result. It carries results, signed links to the output files, and the final file is the entry with the highest block_number. Each link lasts 15 minutes and needs your x-api-key header, so poll again for fresh ones.
job = wait_for_job(task_id, interval=30)if job["status"] != "completed":raise RuntimeError(f"Job {job['status']}")final_block = max(f["block_number"] or 0 for f in job["results"])final = next(f for f in job["results"]if (f["block_number"] or 0) == final_block and f["filename"].endswith(".ndjson"))download = requests.get(final["download_url"],headers={"x-api-key": "YOUR_API_KEY"},allow_redirects=False,timeout=120,)if download.status_code != 200:raise RuntimeError(f"Download failed: {download.status_code}")companies = [json.loads(line) for line in download.text.splitlines() if line.strip()]
We ask for NDJSON alongside the CSV because NDJSON keeps numbers, booleans and nested objects typed, which matters for the leadership list. A row that fails comes back without research and does not fail the job, and in bulk results field confidence arrives as one column holding a JSON string for each row. To keep these fields current after the first run, a monitor re-checks chosen fields on a schedule and records each change with its sources, and monitors are turned on per organisation on request.
How do you get results without polling?
Pass webhook_url, an HTTPS URL on a public host, and Sixtyfour POSTs the result to it when the job completes. It works on /company-intelligence-async, on the sync /company-intelligence call and in a bulk job's config. A single-company callback carries task_id, status, task_type and the same result that job status returns. A bulk callback is a workflow run envelope whose run_id equals the job's task_id, with a results_download_url that works for 15 minutes.
Four details from our outgoing webhooks reference shape the receiver:
- Only completed jobs send a callback. Failed, cancelled and timed-out jobs send none, so polling stays as the fallback for any job whose callback is late.
- A job can send more than one callback, each with a new
event_id, so deduplicate ontask_id, or onrun_idwithprevious_block_idfor a bulk job. - A Company Intelligence callback can arrive before job status reads
completed, and it carries no charge. Readcharge_creditsfrom job status once it does. - Delivery makes 5 attempts, 1, 2, 4 and 8 seconds apart, with 10 seconds per attempt for an async job, and any
2xxresponse counts as success. Redirects are not followed, so a3xxcounts as a failed attempt.
Once your organisation has a signing secret, created by an admin or owner under Settings, then Webhooks, every delivery carries a Sixtyfour-Signature header with a timestamp t and a v1 value: a hex HMAC-SHA256 of the timestamp, a full stop and the raw body, keyed with the secret. This receiver uses the verify function from that page unchanged, and a shortened version of the handler in our webhooks guide:
import hashlibimport hmacimport timefrom flask import Flask, requestTOLERANCE_SECONDS = 300def verify(raw_body: bytes, header: str, secret: str) -> bool:timestamp, signatures = None, []for part in header.split(","):key, _, value = part.strip().partition("=")if key == "t":timestamp = valueelif key == "v1":signatures.append(value)if not timestamp or not timestamp.isdigit() or not signatures:return Falseif abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:return Falseexpected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()return any(hmac.compare_digest(expected, sig) for sig in signatures)app = Flask(__name__)seen_deliveries = set() # use a persistent store in production@app.post("/webhooks/sixtyfour")def sixtyfour_webhook():raw_body = request.get_data()if not verify(raw_body, request.headers.get("Sixtyfour-Signature", ""), SECRET):return "", 401event = request.get_json()key = event.get("task_id") or f"{event['run_id']}:{event['previous_block_id']}"if key in seen_deliveries:return "", 200enqueue(event) # process outside the requestseen_deliveries.add(key) # only after the event is queuedreturn "", 200
SECRET comes from your secret manager, and enqueue hands the event to your own queue so the handler returns 2xx within the 10 seconds a delivery allows. The handler passes verify the raw body before any parsing, and verify refuses a timestamp more than five minutes from your clock. Before a real job runs, send a sample delivery to the receiver:
curl -X POST "https://api.sixtyfour.ai/webhooks/test-events" \-H "x-api-key: YOUR_API_KEY" \-H "Content-Type: application/json" \-d '{"event": "company_enrichment","url": "https://your-server.example/webhooks/sixtyfour","signing": "required"}'
That event is the sample of a Company Intelligence callback, signed as a real delivery would be. A test delivery runs no job and uses no credits, and each call sends a new event, so do not retry it automatically.
How should you design the fields?
The struct is the research brief. Our struct guide puts it directly: "The description is what the agent researches, so say exactly what you want". The habits we follow:
- Ask for something a person could check. A parent company has an answer and a page that states it, while "is this company trustworthy" is a judgement the research can only answer with other people's opinions. Ask the research for the facts a judgement rests on, and keep the judgement for your own logic.
- Make each description specific. The Company Intelligence page notes that "Specific descriptions return better results", and a description that names the evidence you want, such as "named on the company's own site or in its filings", tells the agent what counts.
- Bound anything that changes over time. News from the last 12 months, each with its month, gives a list you can compare between runs, and a field asking for recent news does not.
- Type every field that is not a string. Only a typed object returns the same type on every request,
enumholds the value to youroptions, andobject_listreturns a list of objects with thesubfieldsyou name. Field names must be letters, numbers and underscores. - Put method in
research_plan, up to 100,000 characters of plain-language guidance on where and how to look, such as "Check the About page and the LinkedIn company page". Put who to find inpeople_focus_promptwhenfind_peopleis on.
Two behaviours make typed fields safe to feed into code. A value is converted to a field's type only when nothing is lost in the conversion, so a researched value of about 50 never becomes the integer 50. And every field you ask for is present in the response: one the agent could not establish is null, or an empty list for a Company Intelligence list field. In the struct guide's words, "Values are never invented to satisfy a type."
What should you do with low-confidence or empty fields?
Field confidence is how we decide which values flow straight into a system and which wait for a person. Each score, our confidence scoring page says, "reflects how much assumption is needed to tie the value back to the target person or company", and the docs map the scale to five levels:
| Score | Level | What it means |
|---|---|---|
| 85 to 100 | Confirmed | Near-zero assumption: unique identifiers, direct assertions from authoritative sources |
| 65 to 84 | Established | Minimal assumption; a careful person would rely on it |
| 40 to 64 | Supported | Moderate assumption; the link is reasonable but not tight |
| 15 to 39 | Speculative | Significant assumption; treat it as a hypothesis |
| 0 to 14 | Unreliable | Major assumption or conflicting evidence; do not rely on it |
Pick a threshold for each use. Our docs suggest 65 or higher for cold outreach, and a field that feeds an onboarding decision deserves a higher bar. Then:
- Below your threshold, route the field to a review queue with its value, its justification and its sources, so the reviewer starts where the research stopped. In our example the headcount range at 46 goes to review, and the headquarters at 94 does not.
- A
confidenceofnullmeans the value rests only on what you sent, so it adds nothing you did not already know. An entry that isnullas a whole means the field was not scored, such as a field with no value. - An empty
sourceslist means the value came from a source with no public page, so a reviewer cannot open it. We treat it like a low score. - An empty field means the research did not establish it. Leave it empty, and never fill it from a language model's own knowledge or a guess.
- Expect a company with a common name to score lower across the board, because "The weakest link dominates". Send the website or LinkedIn page with the name: better starting context, our Research API page notes, helps distinguish the intended subject from similarly named companies.
What happens to a field the research left open is your team's call, and a person makes it.
Reading the results fairly
- A source says what it said when it was written. A headcount from an old directory page can be accurate and out of date at once, so a field with a time window is easier to rely on than one without.
- Small and new companies leave less to find. An empty field or a low score is a lead to follow, and it says little about whether the company is real.
- A researched field and a stored record can disagree. The researched one carries its sources, so a reviewer can open them and see which is more recent.
- This post cites no accuracy figure for company research. Measure it on 10 to 20 companies your team already knows well before a full list, as our credits guide suggests for any new workflow.
- Sixtyfour returns what the research found and the pages it found it on. It does not check bank accounts, run consumer background checks or credit reports, or verify identity documents, and anything it could not establish comes back open for a person to decide.
For onboarding a business customer or a supplier, our guides to KYB verification and verifying a vendor cover the checks around the research. If the caller is an AI agent, Company Intelligence is also a tool on our MCP server, sixtyfour_company_intelligence_start, and our post on adding people and company research to an agent covers the setup and what the tools return.
The short version
Send target_company with the name and website, a struct of specific, typed fields and field_confidence set to true. Use the async endpoint in production, store the task_id at once because a repeated start charges again, poll every 10 seconds or slower, and move to signed, deduplicated webhooks once jobs pile up. Upload lists as one bulk job with an Idempotency-Key, and read the NDJSON file for typed values. Route anything under your confidence threshold, anything without sources and anything empty to a person. Sixtyfour researches each company from its name or website and returns the fields you define with the pages behind them, so your team can check what it relies on.