# Company Firmographics Scraper - Revenue, Headcount, HQ (`dami_studio/company-firmographics-scraper`) Actor

Turn a company domain or name into one clean row: legal name, website, street address, industry sectors, exact employee count, exact revenue, founding year, ownership and ticker. No key, no login, no browser. A lookup that cannot be confirmed is flagged free, never billed as your answer.

- **URL**: https://apify.com/dami\_studio/company-firmographics-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.92 / 1,000 company returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Company Firmographics Scraper - Revenue, Headcount and HQ from a Domain

Give it a domain, a company name or an Owler profile link. Get one row back with the legal entity
name, the website, the full street address, the industry sectors, the exact employee count, the
exact revenue in dollars, the founding year, the ownership, the status, and the ticker and exchange
if it is listed. No account, no API key, no login, no browser.

- Employee count and revenue come back as **exact integers**: 7,600 staff and $13,269,000,000, not
  "1,001-5,000" and "$10B+".
- The full postal address, down to the street line and the postcode.
- A company that has been bought reports `status: "Acquired"` and names the parent and the month.
- Every row is checked against the company you asked for. A profile that does not match is handed
  over as a free note saying what it found instead, never billed as your answer.
- Run it with empty input and you get a labelled sample row, free, so you can see the shape first.

### What this does not do

Read this part first, because it is where the surprises are.

- **About one lookup in ten comes back without a company, and that is the honest number.**
  Measured on 50 real domain lookups through this Actor: 45 returned a company, 5 did not. All five
  misses were the same shape: a profile exists, but it is filed under a different website than the
  one asked for, so it was refused rather than guessed at. You are not charged for a miss. The note
  you get instead names the profile that was found, so you can pass its link straight back and take
  it deliberately. Well-known companies land more often than long-tail ones: 24 of 25 on the first
  batch, 21 of 25 on a deliberately obscure second batch.
- **A name on its own is a weaker question than a domain, and it is the one place a wrong company
  can still get through.** Plenty of firms share a name. A name-only lookup is returned when the
  name on the profile matches yours once the legal suffix is stripped, but nothing then confirms it
  is *your* company rather than a namesake. Those rows carry `matchedOn: "name"` so you can see
  which ones rest on that. **If you have the domain, use the domain**: a domain lookup is only
  returned when the website on the profile is that exact domain, so it cannot hand you a namesake.
- **Revenue is an estimate, not a filing.** For public companies it tracks reported figures closely.
  For private ones it is the source's own model. Treat it as a size band that happens to be written
  as a precise number, and do not put it in an audit.
- **Blanks are the source's blanks.** Small and very new companies often have a profile with almost
  nothing on it. `profileCompleteness` is the source's own 0-100 fill rate for that record: 100 is a
  full profile, 25 is a stub with a name and a city. Null means not published. It does not mean
  zero. A company with no published revenue returns `revenueUsd: null`, never `0`.
- **No people.** The underlying profile carries named executives with photographs and their LinkedIn
  URLs, plus a switchboard number. None of that is emitted. This returns facts about companies.
- **No funding rounds, no investors, no news, no competitors, no employee reviews.** One company,
  one row of firmographics.
- **Country codes are not normalised.** The source writes `CA`, `GB` and `USA` on the same field and
  they are published exactly as found rather than converted into a standard that might be wrong. If
  you need ISO-2 everywhere, map `USA` yourself. It is the only three-letter code seen so far.
- **It does not find companies for you.** This is enrichment, not discovery: you bring a list, it
  fills in the columns. There is no "give me every fintech in Berlin" search.

### Input

```json
{
  "companyDomains": ["stripe.com", "deliveroo.co.uk", "octopus.energy"],
  "maxResults": 25
}
```

| Field | What it does |
|---|---|
| `companyDomains` | The company's own website domain. The most reliable way to ask, because the returned profile is checked against it. Scheme and `www.` are stripped for you. |
| `companyNames` | Company names. Looser than a domain; see the note above. If you supply names and domains as two lists of the **same length**, they are paired up and each pair counts as one company rather than two. |
| `companyUrls` | Links of the form `https://www.owler.com/company/shopify`. Taken at face value and always returned if the page exists, so this is the fix for a lookup that came back unsure. |
| `maxResults` | Ceiling on companies returned, whatever the lists hold. Hard limit 1,000. Keep it low while testing, because you pay per company returned. |
| `proxyUrls` | Leave empty. The run brings its own addresses. Fill it in only if you want the traffic to leave through servers you already pay for, as `http://user:pass@host:port`. |

### Output

One row per company. A real row from a real run:

```json
{
  "ok": true,
  "charged": true,
  "recordType": "company",
  "companyName": "Shopify, Inc.",
  "shortName": "Shopify",
  "website": "https://www.shopify.com/",
  "domain": "shopify.com",
  "description": "Shopify is a Canada-based multinational company that provides E-commerce solutions for online retailers.",
  "industrySectors": ["E-commerce"],
  "industryGroups": ["Software, Internet & Computer Services"],
  "employeeCount": 7600,
  "revenueUsd": 13269000000,
  "foundedYear": 2004,
  "ownership": "Public",
  "status": "Independent Company",
  "parentCompany": null,
  "acquiredOn": null,
  "ticker": "SHOP",
  "exchange": "NYSE",
  "street1": "151 O'Connor Street",
  "street2": "Ground Floor",
  "city": "Ottawa",
  "state": "Ontario",
  "country": "CA",
  "postcode": "K2P 2L8",
  "owlerUrl": "https://www.owler.com/company/shopify",
  "companyId": "100740",
  "profileCompleteness": 100,
  "matchedOn": "domain",
  "requestedName": null,
  "requestedDomain": "shopify.com"
}
```

#### Field notes

- **`companyName`** is the legal entity, which is often not the brand. Airtable comes back as
  Formagrid, Inc. and Instacart as Maplebear, Inc. `shortName` is the brand if that is what you
  wanted.
- **`employeeCount`** and **`revenueUsd`** are exact integers or null. Null means the source has not
  published a figure. It is never written as `0`.
- **`status`** is the company's standing: `Independent Company`, or `Acquired`. When it is
  `Acquired`, `parentCompany` names the buyer and `acquiredOn` gives the month, e.g. Deliveroo PLC
  reports `Acquired` / `DoorDash, Inc.` / `05/2025`.
- **`ticker`** and **`exchange`** are filled only for listed companies. A private company returns
  null for both, and `ownership` says `Private`.
- **`industrySectors`** is the specific sector, such as `E-commerce`. `industryGroups` is the broad
  bucket above it, such as `Software, Internet & Computer Services`. Thin profiles often have the
  group and not the sector.
- **`matchedOn`** says which evidence returned this row: `domain` means the website on the profile
  is exactly the domain you gave, and is the strongest; `name` is a name-only lookup, where the name
  is the only evidence there was; `url` means you supplied the link yourself. If you only want the
  strongest, filter on `matchedOn == "domain"`.
- **`profileCompleteness`** is the source's own 0-100 fill rate. Sort by it descending and the thin
  records fall to the bottom.
- **`requestedName`** and **`requestedDomain`** echo what you asked for, so you can join the output
  back onto your input list without matching on names.

### What it costs

**$0.92 per 1,000 companies returned**, plus a **$0.0002 start fee per run**. Flat on every plan, with
no volume tiers, no minimum, no subscription.

**You are charged only for companies that come back with data.** About one lookup in ten cannot be
resolved to a profile, and those return an uncharged row telling you so. On a list of 1,000 domains
you would expect roughly 900 rows and a bill near $0.83, not $0.92.

Nothing else bills. A run that reaches nothing is free past the start fee, a domain that resolves to
no profile is free, and a row dropped because the profile's website did not match the domain you
asked for is free. That last one is deliberate, and it is why a lookup occasionally returns nothing
rather than returning a company with a similar name.

### How it works

It reads the company's public profile page and pulls the structured record the page is built from,
rather than scraping rendered text. That is why the numbers are exact integers instead of parsed
strings, and why a field that is genuinely empty comes back null rather than as a guess.

The site refuses a share of requests outright. Six different connection profiles were measured
against it from the cloud, and they were not close: the one this Actor reaches for first answered
**42 out of 42**, while the profile most scrapers reach for by habit managed 11 out of 45 in the same
job against the same addresses. When a request is refused the run changes both its connection profile
and its outbound address and asks again, up to six times, and a refusal that survives all six is
reported as a free note rather than billed.

Turning a domain into the right profile is the other half. The address is derived from the domain and
the record that comes back is then checked against what you asked for, because a plausible-looking
address can be a real page belonging to a different company with a similar name. That check is the
reason a small number of lookups return nothing: refusing is the right answer when the alternative is
billing you for somebody else's headcount.

### What people use it for

- Filling in the size, revenue and location columns on a CRM export that arrived as nothing but
  company names and domains.
- Scoring inbound sign-ups by company size before a human looks at them.
- Territory and account planning, where a real headcount matters more than a band.
- Checking whether a supplier or counterparty still exists independently, since an acquired company
  reports its parent.
- Building a market map from a list of domains you already have.

### Reading the output

Every run writes three kinds of row and they are easy to tell apart:

- **Real rows** carry `"charged": true` and `"recordType": "company"`. One billed event each.
- **The sample row** carries `"_sample": true`, `"charged": false` and `"recordType": "sample"`.
  There is exactly one, it only appears when the input had no company in it, and it exists so you can
  see the shape before you spend anything.
- **Notes** carry `"_diagnostic": true`, `"charged": false` and an `errorCode` worth switching on:
  `NO_RESULTS` when a profile was found but did not match the company you asked for (the message
  names what it found), `NOT_FOUND` when no profile exists at all, `BLOCKED` when the source refused
  every attempt, `BAD_INPUT` when an entry could not be turned into a lookup, `TIME_BUDGET` when the
  run ran out of time before reaching an entry.

The difference between `NO_RESULTS` and `BLOCKED` is the one that matters: the first means the
company genuinely could not be confirmed, the second means nobody got to look and a re-run will
usually succeed. Neither is charged.

If you only want data, filter on `charged == true`. The count of those rows is the number of events
you were billed for, so the dataset is its own invoice.

### Limitations

- Coverage is good for established businesses and thin for very small, very new or privately held
  local companies. A profile may exist with only a name, a city and a founding year.
- Revenue and employee figures are the source's estimates. They are refreshed periodically, not
  live, and a company that grew this quarter may still read last quarter's headcount.
- A company with no profile returns an uncharged note. There is no fallback to another source.
- Name-only lookups are refused unless the name matches exactly after the legal suffix is stripped,
  so a common or abbreviated name may come back unmatched even though the company is covered.
- The source writes country codes inconsistently (`CA`, `GB`, `USA`) and they are published as found.
- Executives, phone numbers, funding rounds, investors, acquisitions made, competitor lists and news
  are all present on the source record and all deliberately not emitted.
- The hard ceiling is 1,000 companies per run. Split bigger lists across runs.
- A share of requests is refused by the source at random. The run retries across several connection
  profiles and addresses, and reports the ones that never got through without charging for them.

### Questions

**Do I need an account or an API key for anything?**
No. There is no login, no cookie and no key anywhere in this Actor, and you do not need an account
with the source.

**Why is revenue null on a company I know makes money?**
Because the source has not published a figure for it, which is common for private companies. Null
means "not published". It is never written as `0`, so you can tell a genuine blank from a genuine
zero.

**I asked for a domain and got a note saying it was not confident. What now?**
That means a profile was found whose website is a different domain from the one you gave. The note
names it. If it is the company you meant, put its `owler.com/company/...` link in `companyUrls` and
it will be returned. If it is not, you have been saved from a wrong row.

**Can I look up a company by name only?**
Yes, but it is the weakest of the three inputs and it is deliberately strict. Supplying the name and
the domain together as two lists of the same length is better than either alone.

**Is the employee count exact or a band?**
An exact integer, when the source publishes one. That is the main reason to use this rather than a
profile scraper that returns "1,001-5,000 employees".

**What happens on a company that has been acquired?**
`status` reads `Acquired`, `parentCompany` names the buyer and `acquiredOn` gives the month. The rest
of the row still describes the company you asked about, not the parent.

**Will the run fail if the source is down?**
No. You get uncharged notes saying what could not be reached, and the run still finishes as
succeeded. A failed run would still bill the start fee, which would mean paying to be told something
was broken.

**Can I run this on a schedule?**
Yes. Nothing is held between runs, and `requestedDomain` gives you a stable key to join today's rows
onto yesterday's.

# Actor input Schema

## `companyDomains` (type: `array`):

The company's own website domain, such as stripe.com or deliveroo.co.uk. This is the most reliable way to ask, because the domain is what the returned record is checked against - a profile whose website does not match yours is never charged for. Scheme and www. are stripped for you.

## `companyNames` (type: `array`):

Company names, such as Cloudflare or Trader Joe's. Less precise than a domain: several companies share a name, so a name-only lookup is only returned when the name on the profile matches yours after its legal suffix is stripped. If you have both, put the names and the domains in the same order and each pair is treated as one company.

## `companyUrls` (type: `array`):

Links of the form https://www.owler.com/company/shopify. Use these when you already know which profile you want, or when a name or domain lookup came back saying it was not confident. A link is taken at face value and always returned if the page exists.

## `maxResults` (type: `integer`):

A ceiling on how many companies are returned, whatever you put in the lists above. Hard limit 1,000. Keep it low while you are testing - you pay per company returned.

## `proxyUrls` (type: `array`):

Leave this empty. The run brings its own addresses and the cost of them is already inside the price. Fill it in only if you specifically want the traffic to leave through servers you already pay for, as http://user:pass@host:port.

## Actor input object example

```json
{
  "companyDomains": [
    "stripe.com",
    "shopify.com"
  ],
  "companyNames": [],
  "companyUrls": [],
  "maxResults": 10
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: companyName, website, industrySectors, employeeCount, revenueUsd, foundedYear, ownership, status, ticker, exchange, city, state, country, owlerUrl. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companyDomains": [
        "stripe.com",
        "shopify.com"
    ],
    "companyNames": [],
    "companyUrls": [],
    "maxResults": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/company-firmographics-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companyDomains": [
        "stripe.com",
        "shopify.com",
    ],
    "companyNames": [],
    "companyUrls": [],
    "maxResults": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/company-firmographics-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companyDomains": [
    "stripe.com",
    "shopify.com"
  ],
  "companyNames": [],
  "companyUrls": [],
  "maxResults": 10
}' |
apify call dami_studio/company-firmographics-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dami_studio/company-firmographics-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WNYC5CF5kX6sOHQEI/builds/d5IgaYNpW680XZqU8/openapi.json
