# Company Lookup by Domain — Firmographics, Headcount, Tech Stack (`northbell/company-lookup-by-domain`) Actor

Company lookup from a domain, website or LinkedIn URL: one row with LinkedIn profile and real headcount, tech stack, domain age and open jobs. Run it again to see open-job and technology changes since the last run. No login.

- **URL**: https://apify.com/northbell/company-lookup-by-domain.md
- **Developed by:** [Northbell](https://apify.com/northbell) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 companies

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Lookup by Domain — Firmographics, Headcount & Tech Stack

Give it a list of company **domains, website URLs, LinkedIn company URLs or names** and get one clean row per company: the **LinkedIn profile and today's headcount** (industry, size band, HQ, founding year), the **tech stack** read from the homepage, the **domain's age, expiry and registrar**, and the **number of open jobs on LinkedIn**. Four public sources in one row, instead of four Actors and a join. **$5 per 1,000 companies**, no login, no cookies, no API key.

Run it again — or put it on a schedule — and every row also says **what changed since the last run**: open jobs up or down, and which technologies were **added or removed** from the site. Open jobs cannot be looked up for last month afterwards; the run is where they get recorded.

**Need headcount over time or similar companies?** This Actor is the one-row snapshot. For **headcount growth per month** and the **10 similar companies LinkedIn lists**, use the [LinkedIn Company Scraper with Headcount Growth](https://apify.com/northbell/linkedin-company-growth-scraper) ($4 per 1,000, takes the same domains); for lists of lookalikes, [LinkedIn Lookalike Companies](https://apify.com/northbell/linkedin-lookalike-companies).

### What you get

| Field | What it is |
|---|---|
| `type`, `status`, `input`, `matchedBy` | `type` is `company` for every row about a company and `error` for an entry that is not a company at all (error rows have `input`, `message`, `observedAt`). `status` is `matched` (the LinkedIn page was found and verified), `not-found` (no LinkedIn page with that website), `lookup-failed` (LinkedIn could not be read this time) or `not-requested` (you switched LinkedIn off). `matchedBy` says how: `website-link` (the link on the company's own site, and the page lists your domain), `website-link-name` (the company's own site links to the handle that is exactly its name, although the page lists another website), `linkedin-website` (a guessed handle whose page lists your domain), `linkedin-url` or `name`. `input` is what you typed; for a work email only the domain after the @ is kept |
| `domain`, `name`, `linkedinUrl`, `website` | the company and its LinkedIn page |
| `industry`, `companySize`, `employeeCount`, `headquarters`, `founded`, `companyType`, `specialties`, `description`, `followers` | the LinkedIn profile. `employeeCount` is the number LinkedIn counts today; `companySize` is the band the company typed in (`5,001-10,000 employees`) |
| `technologies`, `technologyCategories` | what the homepage runs on, e.g. `["Next.js", "nginx"]`, and the same grouped: `{"Web server": ["nginx"]}` |
| `domainCreatedAt`, `domainExpiresAt`, `domainAgeYears`, `registrar` | from the domain's public registration record: the registrar is a company; **no owner or contact data is read** |
| `openJobsOnLinkedIn`, `openJobsIsMinimum` | open roles LinkedIn lists for the company, worldwide. `openJobsIsMinimum` is `true` when LinkedIn only says "1,000+" |
| `previousOpenJobs`, `openJobsChange` | open jobs at the previous run, and the difference |
| `technologiesAdded`, `technologiesRemoved`, `previousObservedAt` | technologies that appeared or disappeared, and when the previous run was. All `null` on the first run |
| `missing` | every part that could not be read, with the reason, e.g. `{"domainAge": "No RDAP record for notion.so (…)"}`. `{}` when everything worked |
| `observedAt` | when the row was read |

Parts you leave out of **Parts to include** come back as `null`.

### How a domain finds the right company

LinkedIn has no search by website, so the Actor checks instead of guessing:

1. **The link on the company's own site.** Most homepages link their LinkedIn page; it is tried first (`anthropic.com` → `linkedin.com/company/anthropicresearch`, which no name-guessing would find).
2. **Likely handles** (`stripe`, `stripe.com`, `stripehq`, `stripeinc`, `stripeapp`…) when the site has no link.
3. **Every candidate must pass one test: the website on the LinkedIn page is your domain** — or the domain your homepage redirects to (`notion.so` lands on `notion.com`, which is what Notion's LinkedIn page lists). That is why `booking.com` returns Booking.com (17,318 people) and not a café called "Booking".
4. **One exception, flagged in `matchedBy`:** if the company's own site links to the handle that is exactly its name (`figma.com` → `/company/figma`) the page is taken even when LinkedIn lists a different website (Figma's page lists `figma.bot`). The company vouches for its own link. The row says `website-link-name`; filter those out if you want only exact website matches.

If no page passes, you get a **free `not-found` row** — and it still carries the tech stack and domain age it could read. A page with a similar name but another website is never accepted by guessing, only mentioned in `missing.linkedin`. Names (`Scale AI`) and LinkedIn URLs skip the website test (a URL is taken as given; a name takes the first handle that exists), so check `matchedBy` when you feed names.

### Use it for

- **CRM and lead-list enrichment** — a list of domains in, industry, size, HQ, tech and hiring signal out
- **Account research and territory planning** — who is growing, who is hiring, who runs the tool you replace
- **Competitor and prospect monitoring** — schedule it weekly and filter on `openJobsChange` or `technologiesAdded` (for headcount growth, use the Headcount Actor linked above)

### Input

```json
{ "companies": ["stripe.com", "https://www.notion.so", "Scale AI", "https://www.linkedin.com/company/shopify"],
  "include": ["linkedin", "techStack", "domainAge", "openJobs"],
  "maxRequestsPerMinute": 20 }
```

- **companies** — domains, website URLs, work email addresses (only the domain after `@` is used and kept), LinkedIn company URLs or names. Duplicates are merged. IP addresses and internal host names are skipped with an error row.
- **include** — which parts to read (all four by default). *Open jobs* needs the LinkedIn page, so choosing it switches *LinkedIn* on too. A LinkedIn URL or a company name always reads its LinkedIn page, whatever you choose here, because that page is where its website comes from.
- **maxRequestsPerMinute** — LinkedIn requests per minute, 20 by default. A company costs **about 2 LinkedIn requests** (its page, then the open-jobs count) and a few more when its handle has to be found. Plan on roughly 6 to 25 seconds per company at the default. The budget is shared across your runs of this Actor.

History is kept in a named key-value store in your Apify account, so the second run of the same Actor already shows changes. Schedule it (Apify → Schedules) for a regular log. If a part could not be read at one run (say the jobs page refused), its last known value is kept, so the next change is measured against the last time it was read; `previousObservedAt` is always the time of the previous run.

### Pricing

**$5 per 1,000 companies**, plus $0.01 per run.

**Which rows are charged:** a row is charged when the **LinkedIn page was found and verified** (`status: "matched"`). Rows with `not-found` or `lookup-failed`, and error rows, are **free**. A matched row is charged even if another part failed (for example the homepage blocked us); the reason is in `missing`. If you switch *LinkedIn* off in **Parts to include**, a row is charged when at least one of the parts you asked for returned data. The $0.01 run-start fee is the one charge every run pays, even one that returns nothing.

### Notes

- **Headcount** counts LinkedIn profiles that list the company, not payroll. It is a consistent number to track over time even when the absolute figure is off. Some small pages carry no headcount: `employeeCount` is then `null`, never a guess.
- **Open jobs** come from LinkedIn's public job search for the company, always asked for *Worldwide* so the number does not depend on where the Actor runs (LinkedIn shows a different number for one country than for the world: Figma 280 in the United States, 330 worldwide, 5 in Japan, measured on the same day). Checked on Booking.com: the header said 100 and listing the postings returned 100 distinct ones. LinkedIn stops counting at **"1,000+"**: then `openJobsOnLinkedIn` is 1000, `openJobsIsMinimum` is `true`, and `openJobsChange` stays `null` (a change against "at least 1,000" would be invented). A company with no listed roles returns 0. If the count cannot be read, the first page of the job list is used instead and `openJobsIsMinimum` is `true` when that page is full.
- **Tech stack** is detected from one request to the homepage: HTML, script and stylesheet addresses, the generator tag, response headers and cookie names (never their values). 154 technologies in 34 categories. Things loaded only after a click, or only for signed-in users, are not seen. A site that answers with bot protection becomes `missing.techStack`, and a bot-check page is never read as the site's stack. `technologiesAdded` and `technologiesRemoved` compare with the previous run.
- **Domain age** comes from RDAP, the registries' public registration record. Some endings publish none: in my test **.io, .de, .jp, .co.jp, .us, .so and .gg had no record** while .com, .net, .ai, .dev, .app and .co.uk worked. For those the dates and registrar are `null` and `missing.domainAge` says so — [WHOIS Domain Lookup](https://apify.com/northbell/whois-domain-lookup) covers them.
- **Filled only in some cases:** `specialties` (many pages do not list any, e.g. Stripe), `founded`, `headquarters` and `companyType` (not on every page), `description` (shortened to 600 characters). A company whose LinkedIn page lists no website of its own, or only a social or link page, has no `domain` when you gave a LinkedIn URL or a name, so its tech stack and domain age are `null` with a reason.
- **A different website on LinkedIn.** If the company's LinkedIn page lists another website than your domain and the company's own site does not link to it under its exact name, the row is `not-found` and `missing.linkedin` names that page. Pass its LinkedIn URL instead.
- **No people, no contacts.** The Actor reads company pages only: no employee names or profiles, no email addresses or phone numbers (removed from free text), no domain owner or WHOIS contacts.
- **Measured on October 8, 2026:** the three prefilled domains (stripe.com, notion.so, figma.com) plus four more inputs (a company name, a work email, example.com and an IP address) → 5 matched rows, 1 free `not-found` row (example.com has no company page) and 1 free error row (the IP address). 19 LinkedIn requests took about 15 seconds on a fresh request budget; longer lists are paced by the 20 per minute default. A second run on the same account showed `employeeCountChange` and `openJobsChange` of 0 and empty `technologiesAdded` / `technologiesRemoved` lists, as expected a few minutes apart.

More from the same family: [LinkedIn Company Scraper with Headcount Growth](https://apify.com/northbell/linkedin-company-growth-scraper) (headcount over time for companies you already know), [Domain to LinkedIn Company URL](https://apify.com/northbell/domain-to-linkedin-company) (just the verified page), [Tech Stack Detector](https://apify.com/northbell/tech-stack-detector) (any site, with versions), [WHOIS Domain Lookup](https://apify.com/northbell/whois-domain-lookup) (expiry, nameservers, renewals), [LinkedIn Company Jobs Scraper](https://apify.com/northbell/linkedin-company-jobs-scraper) (every open role, not just the count) and [LinkedIn Lookalike Companies](https://apify.com/northbell/linkedin-lookalike-companies) (companies like these).

### For AI agents

`{"companies": ["stripe.com"]}` returns one row with `name`, `employeeCount`, `technologies`, `domainAgeYears` and `openJobsOnLinkedIn`. Rows have `type`: `company` or `error`; check `status` (`matched` is the verified one) and `missing` for parts that could not be read. Run again with the same account to get `employeeCountChange`, `openJobsChange`, `technologiesAdded` and `technologiesRemoved`.

# Actor input Schema

## `companies` (type: `array`):

One company per line: a domain (stripe.com), a website URL, a LinkedIn company URL or a company name (Scale AI). A domain is matched to the LinkedIn page whose own website is that domain; names are guessed, so the row says how each was matched.

## `include` (type: `array`):

Which parts to read for each company. All four by default. Open jobs needs the LinkedIn page, so choosing it switches LinkedIn on too. Parts you leave out come back as null and make the run faster.

## `maxRequestsPerMinute` (type: `integer`):

Each company uses about 2 LinkedIn requests (the company page and the open-jobs count), a few more when its handle has to be found. Kept polite by default. The budget is shared across your runs of this Actor, so parallel runs do not stack up.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "notion.so",
    "figma.com"
  ],
  "include": [
    "linkedin",
    "techStack",
    "domainAge",
    "openJobs"
  ],
  "maxRequestsPerMinute": 20
}
```

# Actor output Schema

## `companies` (type: `string`):

Firmographics, tech stack, domain age, open jobs and the change since the last run.

# 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 = {
    "companies": [
        "stripe.com",
        "notion.so",
        "figma.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/company-lookup-by-domain").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 = { "companies": [
        "stripe.com",
        "notion.so",
        "figma.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("northbell/company-lookup-by-domain").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 '{
  "companies": [
    "stripe.com",
    "notion.so",
    "figma.com"
  ]
}' |
apify call northbell/company-lookup-by-domain --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,northbell/company-lookup-by-domain"
        }
    }
}
```

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/QgQmSWZYmDE6afhbR/builds/HjmmLusDgLWkAnXYy/openapi.json
