Company Search API
Company Search finds companies, not pages. Describe the companies you want in plain language, for example "Series A fintech startups in Berlin building payment infrastructure". Desearch looks for their websites, reads each homepage, and scores how well the company fits. It then reads the company's contact, about, team, and imprint pages for emails, phones, and social profiles, finds its leaders (CEO, founders, CTO, and CFO by default) with their LinkedIn profiles, infers their emails from the company's address pattern, and checks every email.
Company Enrich collects the same data for companies you already know. You send domains, and you get one company per domain, without a fit score.
Every contact keeps the page it came from. Public data only: what the company publishes on its own site, what web search returns, and public LinkedIn profiles. An inferred email is a guess from the company's address pattern and says so (source: "inferred").
Who this page is for
Humans
Start a task with one POST, then poll it, read the companies it delivered, and download them as CSV. The task runs in the background, and companies arrive while it runs. Full schemas are in the API Reference.
Agents / tool authors
A task runs in the background. The POST returns a task_id at once, with no companies in it. Poll GET /desearch/companies/tasks/{task_id} and read GET /desearch/companies/tasks/{task_id}/results with the cursor it returns. A company is published before every stage has run, and later stages fill it in: read again without cursor to get the latest version of every company. The POST is the only billed call. Send an Idempotency-Key header so a retried call does not start, or pay for, a second task.
Company Search or Company Enrich?
| Company Search | Company Enrich | |
|---|---|---|
| Route | POST /desearch/companies/search | POST /desearch/companies/enrich |
| You send | A description (query) | 1 to 200 domains or URLs (domains) |
| You get | Up to count companies that match, each with a fit score | One company per domain you send, with score: null |
Task search_type and platform | company_search, company | company_enrich, company_enrich |
| Rate | $90 per 1,000 companies ($0.09 each) | $50 per 1,000 companies ($0.05 each) |
| Minimum billed per task that delivers any company | 2 companies | 1 company |
Use Company Search to build a list of accounts that you do not have yet. Use Company Enrich when you already have the list, from a CRM, a spreadsheet, or a Company Search, and need contacts and leaders for it. Both return the same company fields, run on the same task routes, and bill only the companies they deliver.
Which surface should I use?
| Goal | Use | Not this |
|---|---|---|
| A list of companies that match a description, with their contacts and leaders | Company Search (this page) | Web Search, which returns pages |
| Contacts and leaders for domains you already have | Company Enrich (this page) | Extract on each page yourself |
| People or creators by what they post or how they describe themselves | People Search | Company Search |
| The text of one page you already have | Extract | Company Enrich |
Base URL: https://api.desearch.ai
Auth: Authorization: $DESEARCH_API_KEY (raw key in examples).
First call: Company Search
The response is 202 Accepted with the new task. Its status is queued, and reserved_cost_usd is the amount reserved for count companies. Keep task_id for every call below. Repeating the call with the same Idempotency-Key returns 200 with the same task and is not charged again.
Request body fields:
| Field | Meaning |
|---|---|
query | Required. The companies you are looking for, in plain language: what they do, where, how far along. 3 to 1000 characters. |
count | How many companies to deliver. 5 to 200, default 25. The task reserves this many and bills only the companies it delivers. |
min_score | Only deliver companies with at least this fit score, 0 to 100. |
exclude | Companies to leave out, as domains or URLs, for example ones you already contacted. Each is reduced to its host without www.. Up to 1,000. |
roles | The leaders to look for: any of ceo, founder, cto, cfo, coo, cmo, president, owner, managing_director. Default ["ceo", "founder", "cto", "cfo"]. A managing director (Geschäftsführer) counts as ceo when ceo is asked for. |
infer_emails | Default true. Guess leaders' emails from the company's address pattern (for example first.last), learned from the addresses found on its site. Inferred emails say source: "inferred" and carry a confidence. |
verify_emails | Default true. Check every email: syntax, mail server (MX), and, for inferred ones, a web search. Each email then has a verification. Turn it off to finish sooner without it. |
filters.countries | Where the companies should be: ISO 3166-1 alpha-2 codes (DE) or country names. Scoring treats them as a location criterion. Up to 20. |
filters.require | Only deliver companies that have all of these: email, phone, linkedin (the company's LinkedIn page), leader (a leader with one of roles), leader_email (a leader with an email). A company that does not have them is not delivered and not billed. |
metadata | Your own labels for the task, for example {"campaign": "q4"}. Returned on the task and usable as a filter on the task list. The task never uses them. See Label tasks with metadata below. |
The body accepts only these fields. An unknown field fails validation with 422.
Request header, the same on both create routes:
| Header | Meaning |
|---|---|
Idempotency-Key | Optional. Any unique string, for example a UUID. Surrounding whitespace is removed and the value is cut to 200 characters. Sending the same key again returns the first task (HTTP 200) and refunds this call's reservation, so it is not charged. A new key starts a new task (HTTP 202). |
First call: Company Enrich
| Field | Meaning |
|---|---|
domains | Required. 1 to 200 company domains or URLs, such as acme.com or https://www.acme.com/about. Each is reduced to its host without www. and lowercased, and duplicates are removed. An IP address, localhost, or anything that is not a public host name fails with 422. |
roles, infer_emails, verify_emails, metadata | The same as on Company Search. |
On the task, count is the number of domains left after deduplication, and query is a short label of them, such as acme.com, beta.io or acme.com, beta.io and 3 more. request.domains lists the normalized domains.
In Python, call company_enrich(domains=[...]). In JavaScript, call companyEnrich({ domains: [...] }). Both return the same task as Company Search.
How a task runs
Each company goes through five stages. A company is published as soon as the stages its request needs have run, and the later stages fill it in while you read it.
| Stage | What it does |
|---|---|
profile | Reads the homepage: name, description, structured data, social profiles, and the emails and phones on it. Company Search also scores the company here. |
contacts | Reads up to 4 more pages of the company's own site (contact, imprint or impressum, team, about) for emails, phones, and social profiles. |
people | Finds the leaders with one of roles: first on the company's own pages, where the quote that names them must be on the page, then on LinkedIn for the roles still missing. |
email_patterns | Learns the company's address pattern from the names and emails it found, and infers the leaders' emails. Skipped when infer_emails is false. |
email_check | Checks every email: syntax, mail server, and, for an inferred email, a web search for the address. Skipped when verify_emails is false. |
When a company is published:
filters.require contains | Published after |
|---|---|
| Nothing (and every Company Enrich task) | profile |
email, phone, or linkedin | contacts |
leader | people |
leader_email | email_check |
A company that does not meet filters.require once its stage has run is not delivered and not billed. The task counts it in progress.filtered, for example no_email.
enrichment.status on each company is partial while any stage is pending, then complete. enrichment.stages shows each stage as pending, done, failed, or skipped. On the task, stage moves through planning, searching, processing, and finalizing, and is enriching once every company is published and only the later stages of some of them are left. The task ends only after those stages have run, so read the companies after it ends to get their final version. When the task's time limit stops the later stages, the stages left are skipped, enrichment.errors says the time limit was reached, and the task has the enrichment_time_limit warning.
Poll the task
Poll every few seconds. The task moves through queued, running, and sometimes pausing, paused, or cancelling, then ends as completed, partial, failed, cancelled, or expired.
| Task field | Meaning |
|---|---|
task_id, search_type, platform | The task and the product it runs: company_search and company, or company_enrich and company_enrich. |
status, stage, progress | Where the task is. |
query | Company Search: the description. Company Enrich: a short label of the domains. |
count, result_count | Companies asked for (Company Enrich: the domains left after deduplication) and companies delivered so far. |
request | The normalized request. Company Search: query, count, min_score, exclude, roles, infer_emails, verify_emails, filters. Company Enrich: domains, roles, infer_emails, verify_emails. |
metadata | The labels sent with the task, or {} when it had none. |
interpreted | Company Search: how the description was understood, with its criteria and the queries it ran. |
completion_reason | Why the task ended, below. null while it is queued, running, or paused. |
coverage_limited | true when the task stopped on a budget (completion_reason cost_limit, candidate_budget, or time_limit) before reaching count: more companies may match than it returned. Default false. status is still completed. |
warnings | What made part of the task less reliable without failing it, below. [] when nothing did. |
reserved_cost_usd | Amount reserved when the task was created. |
final_cost_usd, refunded_usd | Final cost and the returned part of the reservation, once the task has ended. |
created_at, started_at, paused_at, finished_at | When the task was created, started, last paused, and ended. |
results_expire_at | When the results stop being available, 30 days after the task ends. |
links | Paths for this task, its results, and its export. |
completion_reason is one of:
| Value | Meaning |
|---|---|
count_reached | It delivered count companies. |
all_domains_processed | Company Enrich: it read every domain. |
rounds_exhausted | It ran every search it planned and delivered fewer than count. |
no_candidates | Its searches found no company to check. |
zero_qualified | It checked the companies its searches found and none fit. |
candidate_budget | It checked as many candidates as a task of this count may. |
cost_limit | What searching cost reached what its results would bill, so it stopped. |
time_limit | It ran for its maximum time. |
cancelled | It was cancelled. |
failed | An error ended it. |
progress holds counters while the task runs:
| Key | Meaning |
|---|---|
candidates_found, candidates_processed, qualified | Companies the searches found, companies checked, and companies that fit. |
filtered | Companies left out, by reason, for example unreachable (the site could not be read), not_a_company, below_min_score, and no_<requirement> for a filters.require entry, such as no_email. |
enrichment | published, complete, pending, and failed: companies delivered, and how many of them have every stage done. |
email_checks | checked, valid, invalid, risky, and unknown: email checks run, by result. |
stop_reason | The worker's internal reason for stopping. Prefer completion_reason and coverage_limited. |
Each item in warnings has a code and a message. Codes the API uses today:
code | Meaning |
|---|---|
planning_fallback | The search was planned without AI, so results may be broader. |
ai_scoring_unavailable | AI scoring was unavailable, so companies were matched by the search's words only. |
enrichment_time_limit | Some companies were not fully enriched before the task's time limit. Their remaining stages are skipped, with an enrichment.errors entry. |
linkedin_verification_unavailable | Leaders found through LinkedIn search could not be checked against their LinkedIn profiles. |
Treat an unknown code as informational and show its message.
Read the results
Companies arrive while the task is still running. Read them in the order they were found. Leave out cursor on the first call, then pass each response's next_cursor back as cursor:
The response has task_id, status, results (the companies), next_cursor, and has_more. limit is 1 to 500, default 100.
- When
has_moreisfalse, you have every company delivered so far. Keep the lastnext_cursorand call again later to get only the companies delivered since. - A cursor returns only companies delivered after it. Companies you already have are filled in place by the later stages. To get their latest version, read again without
cursor, or wait until the task has ended and read once. - Check the task, not the results page, for the end: the task has ended when its
statusiscompleted,partial,failed, orcancelled.
Reading results
A finished task says why it stopped, and each company says how complete it is and what its own pages confirm. Check these before you use the list:
| Field | What it tells you | What to do |
|---|---|---|
completion_reason | count_reached or all_domains_processed: it delivered what you asked for. rounds_exhausted: it ran every search it planned and delivered fewer. no_candidates or zero_qualified: it found no company, or none that fit, and costs nothing. | When it delivered fewer companies than you asked for, describe them more broadly. |
coverage_limited | true when a budget stopped the task before count, so more companies may match. | To look further, start a new search and put the domains you already have in exclude. |
enrichment.status, enrichment.stages | partial while later stages are still to run, complete when none is pending. A stage is failed when it could not run, and skipped when the request turned it off or the time limit stopped it. | On a running task, read again without cursor for the latest version. Check enrichment.errors for stages that failed. |
primary_email, primary_contact | The company's best email and the person to write to first. | Use these for outreach. Check verification.status before you send. |
emails[].verification.status | valid, invalid, risky, or unknown. | Do not send to invalid. Treat risky and unknown with care. |
people[].verified | true when a quote on the company's own site or the person's current LinkedIn position shows the role. | Check evidence for leaders that are not verified. |
score.criteria_checks, score.fully_verified | What the company's own pages confirm, criterion by criterion. Company Search only. | Read the checks that are not met to see what is unconfirmed. The company still qualified. |
Example company
A complete Company Search result:
A Company Enrich result has the same fields with "score": null, and found_by lists the domain you sent (kind: "domain"). Fields that a later stage has not filled in yet are empty lists or null. Treat a missing value as unknown, not as absent.
Company fields
| Field | Meaning |
|---|---|
id, platform, handle, domain | company:<domain>, always company, and the company's domain (twice). |
url, name, description, logo_url, industry | From the homepage. |
location | country, city, and address, when the site states them. |
emails | The company's emails, below. |
phones | Up to 4 phone numbers, below. |
socials | The company's social profiles, below. |
pages | The pages that were read, each with type (home, contact, about, team, imprint, legal, other) and url. |
people | Leaders with one of the request's roles, below. |
primary_email | The company's best email, an item like those in emails, or null. |
primary_contact | The person to contact first, or null. |
contact_coverage | What was found: email, phone, linkedin, leader, and leader_email, the same names as filters.require, each true or false. |
enrichment | status, stages, and errors, below. |
score | The fit score, below. null on Company Enrich. |
found_by | How the company was found, below. |
Emails
Each item in emails, in people[].emails, and primary_email has:
| Field | Meaning |
|---|---|
value | The address. |
type | personal, leadership, sales, general, press, support, careers, legal, or other. Two names joined by ., -, or _ read as personal. A single word that is no known role is other until it matches a leader's name, so anna@ is personal for Anna Schmidt. |
source | website, search, linkedin, or inferred. |
source_url | The page the email was found on. null when it was inferred. |
on_domain | true when the address is on the company's domain. |
person | The person the address belongs to, when known. |
confidence | Inferred emails only: high when the pattern was learned from this company's own addresses, low for a common pattern that was not learned. |
pattern | Inferred emails only: the pattern used, such as first.last. |
verification | The email check, or null until email_check has run (and always when verify_emails is false). |
verification has status, checks, and checked_at:
| Field | Values |
|---|---|
status | valid, invalid, risky (a catch-all domain, or no MX record but an A record), or unknown (nothing could confirm or refute it). |
checks.syntax | true when the address is well formed. |
checks.mx | ok, implicit (no MX record, but the domain has an A record), none, or error. |
checks.smtp | accepted, rejected, catch_all, unreachable, or skipped when no SMTP check ran. |
checks.web | found when a web search finds the address, not_found, or skipped (addresses that were not inferred). |
Which emails are kept:
- An address is kept when it is on the company's domain, at a free mail provider, or on a domain with the same name (
acme.iolistingacme.com). Addresses of other organizations listed on imprint pages, such as regulators, ombudsmen, or processors, are dropped. - System addresses (
noreply@,postmaster@,bounce@,mailer-daemon@) and junk (image names such aslogo@2x.png, example and placeholder addresses, vendor addresses such as@sentry.ioor@wixpress.com) are dropped. - After
email_check, a person keeps at most one low-confidence inferred email: the firstvalidone, else thefirst.lastguess.
primary_email is only ever a personal, leadership, sales, or general address. support@, press@, jobs@, privacy@, and other addresses stay in emails but never become the company's primary email. An inferred email becomes the primary email only when its check is valid.
Phones and socials
phones[] field | Meaning |
|---|---|
value | E.164 when the number parses, otherwise the digits as written. |
type | main, sales, support, mobile, or other. Fax numbers are dropped. |
raw | The number as written on the page. |
source, source_url | Where it was found. |
A company has at most 4 phones: tel: links and structured data first, then numbers labelled in the page text.
socials[] field | Meaning |
|---|---|
type | linkedin, x, facebook, instagram, github, youtube, tiktok, crunchbase, discord, telegram, or medium. |
url, handle | The profile URL and its handle. Share and intent links are dropped. |
source, source_url | Where it was found. |
People
Each item in people is a leader:
| Field | Meaning |
|---|---|
name, title | The person and their title as written, such as Co-founder & CEO. |
roles | Which of the request's roles the person holds, such as ["ceo", "founder"]. A managing director counts as ceo when ceo was asked for. |
linkedin_url, x_url, github_url | Their profiles, when found. |
emails, phones | Their own contacts, in the same shape as the company's. |
evidence | What shows the role: quote, source_url, and source (website, linkedin, or search). |
verified | true when a quote on the company's own site or the person's current LinkedIn position shows the role. |
primary_contact has name, title, email, linkedin_url, and reason, a short line on why this person was picked, such as CEO, email inferred from the company's pattern and verified.
Enrichment
| Field | Meaning |
|---|---|
enrichment.status | partial while any stage is pending, then complete. |
enrichment.stages | profile, contacts, people, email_patterns, and email_check, each pending, done, failed, or skipped. skipped means the request turned the stage off, such as infer_emails: false, the stage had nothing to do, or the task's time limit stopped it. |
enrichment.errors | Stages that failed or were stopped, each with stage and message. The company keeps what the other stages found. |
Score
Company Search scores each company against the description. Company Enrich does not, and its score is null.
| Field | Meaning |
|---|---|
value | The fit score, 0 to 100. min_score filters on it. |
qualified, relevance | Whether the company fits, and high, medium, or low. |
summary | One line on why this company matched. |
matched_signals, reasons, concerns | What matched, why it fits, and what is uncertain. |
criteria_checks | One check per criterion: what the company's own pages confirm. Information only: a qualified company can still have criteria that are unknown. |
fully_verified | true when every criterion is met and no disqualifier (is_disqualifier) is. |
Each check in criteria_checks has:
| Field | Meaning |
|---|---|
criterion | One of the search's criteria, word for word. |
verdict | met: a quote from the company's pages shows it (quotes are checked word for word; one that is not there becomes unknown). not_met: the evidence contradicts it. unknown: nothing found shows it either way. For a disqualifier, met means it applies. |
quote | The verbatim text behind a met verdict, or null. |
source_url | The page the quote is on, or null. |
reason | A short explanation of the verdict. |
is_disqualifier | true when the criterion is a disqualifier, so met means it applies. |
Found by
Each item in found_by has query (the search that found the company), url, title, and kind: direct (a hit on the company's own site), list (the company was listed on a page the search read, such as a directory), or domain (Company Enrich: a domain you sent).
Download CSV or JSON Lines
format is csv (default) or jsonl. The export has every company delivered so far, in the order they were found. Fields that later stages have not filled in yet are empty, so export a running task again after it ends. The file is named company-search-<task_id>.csv or company-enrich-<task_id>.csv (.jsonl for JSON Lines).
format=jsonl returns one company per line, with every field above.
The CSV has one row per company and these columns:
| Columns | Meaning |
|---|---|
domain, name, url, score | The company and its fit score (empty on Company Enrich). |
country, city, industry | From location and industry. |
primary_email, primary_email_type | The company's best email and its type. |
emails, phones | Every company email and phone, separated by ; . |
linkedin, x, facebook, instagram, github, youtube, tiktok | The company's profile URL on each network. |
ceo_name, ceo_title, ceo_email, ceo_email_status, ceo_linkedin | The CEO, or a managing director when there is no CEO. |
founder_*, cto_*, cfo_* | The same five columns (_name, _title, _email, _email_status, _linkedin) for the founder, the CTO, and the CFO. |
fully_verified | score.fully_verified, or empty when it is unknown. |
unverified_criteria | The criteria that are not met, plus the disqualifiers that are met, separated by ; . |
enrichment_status | partial or complete. |
A leader's email column shows their first valid email, else the first one not known to be invalid, else the first. Leaders who hold none of these roles, such as a COO, are only in the JSON Lines export. A managing director fills the CEO columns when the company has no CEO.
Results stay available for 30 days after the task ends. After that, results and export return 410.
Pause, resume, or cancel
| Call | What happens |
|---|---|
POST /desearch/companies/tasks/{task_id}/pause | A queued task pauses at once. A running task finishes its current batch first (pausing, then paused). Companies delivered so far stay available. A task paused for more than 7 days is cancelled. |
POST /desearch/companies/tasks/{task_id}/resume | Continues a paused task from where it stopped. It is not charged again. |
POST /desearch/companies/tasks/{task_id}/cancel | Stops the task. You pay only for the companies it delivered, and a task cancelled before it starts costs nothing. Companies delivered so far stay available for 30 days. |
When the task's status does not allow the action, the call returns 409 with detail and the current task.
List tasks
GET /desearch/companies/tasks lists your company's Company Search and Company Enrich tasks, newest first.
| Parameter | Meaning |
|---|---|
type | search (Company Search) or enrich (Company Enrich). Leave it out for both. |
status | Comma-separated statuses, for example running,paused. |
metadata.<key> | metadata.<key>=<value>: only tasks with that label, below. Up to 3. |
limit, cursor | Page size (1 to 100, default 20) and the next_cursor from the previous page. |
People Search tasks are not listed here, and a People Search task_id returns 404 on the company task routes (and a company task_id returns 404 on the People Search routes).
Label tasks with metadata
Add metadata to a Company Search or Company Enrich request to tag the task with your own labels, such as the campaign, workspace, or customer it runs for. The task never reads them, so labels do not change what is found or what it costs.
A task must match every filter exactly. When you page with cursor, send the same filters on every page.
| Rule | Limit |
|---|---|
| Labels per task | Up to 10 keys. |
| Keys | 1 to 40 characters: lowercase a-z, 0-9, _, ., or -. |
| Values | Strings of up to 200 characters. |
| Filters per task list call | Up to 3 metadata.<key>=<value> parameters. |
A request whose metadata breaks these rules fails with 422. On the task list, an invalid metadata.<key> filter returns 400.
Billing
- Starting a task is the only billed call. Company Search reserves
countcompanies at $0.09 each ($90 per 1,000). Company Enrich reserves one company per domain you send at $0.05 each ($50 per 1,000). The response carries the usualX-Desearch-Cost-Usd,X-Desearch-Usage-Count,X-Desearch-Service(Company SearchorCompany Enrich), andX-Desearch-Currencyheaders, and the task showsreserved_cost_usd. - When the task ends, you pay only for the companies it delivered, with a minimum of 2 for Company Search and 1 for Company Enrich when it delivers any. You never pay more than the reservation. The rest of the reservation is returned automatically, including duplicate domains, and the task shows
final_cost_usdandrefunded_usd. - A company left out by
min_scoreorfilters.require, or whose site cannot be read, is not delivered and not billed. A task that delivers no companies is free, and so is a task cancelled before it starts. - Polling, results, exports, pause, resume, cancel, and the task list are free.
- The same
Idempotency-Keyreturns the first task and is not charged again. 402means the balance cannot cover the reservation.429means too many of your People Search and Company Search tasks are already queued, running, or paused.
Rates are on the pricing page, as of 28 October 2026. Console shows the rates for your account.
Errors
| Status | Meaning |
|---|---|
400 | Unknown type, status, or cursor, or an invalid metadata.<key> filter on the task list. |
401 | Missing API key, or a key that is not valid. |
402 | Not enough credits for the reservation. |
403 | A malformed Authorization value, such as an unreplaced placeholder like YOUR_API_KEY. See Auth and API keys. |
404 | Task not found, including a People Search task_id on these routes. |
409 | Pause, resume, or cancel does not apply to the task's current status. The body has detail and the current task. |
410 | Results expired, 30 days after the task ended. |
422 | Invalid request body or parameters: for example a query under 3 characters, count outside 5 to 200, an unknown role or filters.require value, more than 200 domains, a domain that is an IP address or localhost, an unknown field, or metadata that breaks the label rules. |
429 | Too many People Search and Company Search tasks already queued, running, or paused. |
SDKs and MCP
- Python:
desearch-py1.10.0 addscompany_search,company_enrich,list_company_tasks,get_company_task,get_company_results,pause_company_task,resume_company_task,cancel_company_task,export_company_results, and the helperswait_for_company_taskanditer_company_results, with the modelsCompany,CompanyTask, and the others. A 409 from pause, resume, or cancel raisesCompanyTaskConflictError(.detail,.task), and a timeout inwait_for_company_taskraisesCompanyTaskTimeoutErrorwhile the task keeps running. 1.10.0 is not on PyPI yet; install it from GitHub withpip install "desearch-py @ git+https://github.com/Desearch-ai/desearch.py@864469c5f8e477ef0ef2142063a558c3849850aa". See the Python SDK. - JavaScript:
desearch-js1.11.0 addscompanySearch,companyEnrich,listCompanyTasks,getCompanyTask,getCompanyResults,pauseCompanyTask,resumeCompanyTask,cancelCompanyTask,exportCompanyResults, and the helperswaitForCompanyTaskanditerCompanyResults, with types such asCompanyandCompanyTask. Pause, resume, and cancel throwCompanyTaskConflictErroron HTTP 409. See the JavaScript SDK. - MCP: from
desearch-mcp-server0.6.0, the toolscompany-search,company-enrich,company-search-status,company-search-tasks,company-search-pause,company-search-resume, andcompany-search-cancel. See MCP.