# AI Company Enrichment - Website to Firmographics (`ceo.sss/ai-company-enrichment`) Actor

Turn a list of company websites into clean firmographics: industry, what they sell, who they sell to, size band, pricing model, contacts, socials, tech hints. One LLM-verified JSON record per company, every judgment with evidence.

- **URL**: https://apify.com/ceo.sss/ai-company-enrichment.md
- **Developed by:** [Swaraj Samant Singh](https://apify.com/ceo.sss) (community)
- **Categories:** Lead generation, AI, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 company enricheds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## AI Company Enrichment - Website to Firmographics

Turn a list of company websites into clean firmographics: industry, what they sell, who they sell to, size band, pricing model, contacts, socials, tech hints - JSON/CSV. One LLM-verified record per company with evidence.

The actor reads the company's own site (homepage + about, pricing, careers, contact, blog), pulls the hard facts deterministically (no model involved: emails, phones, social links, tech fingerprints, open roles, last blog post, language, country hints), then makes exactly one LLM call to fill the judgment fields. Every judgment field comes with `evidence`: a quote from the pages or the page URL. Quotes that do not actually appear in the crawled text are thrown away, so a null evidence entry tells you the value is the model's inference rather than something the site says.

Companies that cannot be fetched or classified get a row with `enrichment_error`. No guessed records, and no charge for them.

### What you get per company

```json
{
  "website": "https://linear.app",
  "finalUrl": "https://linear.app/",
  "companyName": "Linear",
  "tagline": "The product development system for teams and agents",
  "description": "Linear provides a purpose-built system for modern product teams and AI agents to plan, build, and ship software products with speed and clarity.",
  "industry": "Software & SaaS",
  "subIndustry": "Product management and issue tracking software",
  "businessModel": "B2B",
  "whatTheySell": "Cloud-based product development and workflow tools that combine planning, issue tracking, and AI agents for software teams.",
  "targetCustomer": "Software and product development teams at companies that need structured workflows to plan, build, and ship products.",
  "companySizeBand": "201-1000",
  "sizeReasoning": "Serving more than 40,000 companies and having 26 open roles suggests a mid-sized team; this is an estimate.",
  "pricingModel": "subscription",
  "hasFreeTrial": false,
  "notableProducts": ["Linear Agent", "Triage Intelligence", "Code Intelligence", "Linear Insights", "Linear Asks"],
  "keywords": ["product development", "issue tracking", "ai agents", "software teams", "planning", "workflow", "project management", "product tool", "triage intelligence", "linear agent"],
  "techStackHints": ["Next.js", "React", "Cloudflare", "Ashby"],
  "emails": ["hello@linear.app"],
  "phones": [],
  "socials": { "linkedin": null, "twitter": "https://x.com/linear", "github": "https://github.com/linear", "youtube": "https://www.youtube.com/@linear", "instagram": null, "facebook": null },
  "addressHints": null,
  "country": null,
  "countrySource": null,
  "language": "en",
  "openRolesCount": 26,
  "blogLastPostDate": "2026-08-13",
  "careersUrl": "https://linear.app/careers",
  "pricingUrl": "https://linear.app/pricing",
  "contactUrl": "https://linear.app/contact",
  "aboutUrl": "https://linear.app/about",
  "blogUrl": "https://linear.app/insights",
  "pagesFetched": ["https://linear.app/", "https://linear.app/about", "https://linear.app/pricing", "https://linear.app/careers", "https://linear.app/contact", "https://linear.app/insights"],
  "confidence": 0.91,
  "evidence": {
    "tagline": "The product development system for teams and agents",
    "whatTheySell": "A new species of product tool. Purpose-built for modern teams with AI workflows at its core, Linear sets a new standard for planning and building products.",
    "companySizeBand": "Our team is distributed across North America and Europe, and we're continuing to grow internationally.",
    "hasFreeTrial": "Pricing Free $0 Free for everyone",
    "targetCustomer": null,
    "...": "one key per judgment field"
  },
  "llmModel": "gpt-4o-mini",
  "enrichedAt": "2026-08-18T10:12:41.126Z",
  "enrichment_error": null
}
```

| Field | Source | Meaning |
|---|---|---|
| `website`, `finalUrl` | input / HTTP | What you passed and where it redirected (`notion.so` -> `notion.com`) |
| `companyName`, `tagline`, `description` | LLM, falls back to JSON-LD / OG / title | Name as the company writes it, its own one-liner, a 1-2 sentence factual summary |
| `industry` | LLM | One of 30 fixed values (list below), so you can group and filter |
| `subIndustry` | LLM | Short free text, e.g. "Online brokerage and trading platform" |
| `businessModel` | LLM | `B2B` / `B2C` / `B2B2C` / `marketplace` / `agency` / `nonprofit` / `other` |
| `whatTheySell`, `targetCustomer` | LLM | One sentence each |
| `companySizeBand`, `sizeReasoning` | LLM | `1-10` / `11-50` / `51-200` / `201-1000` / `1000+` plus the signal it used. Always an estimate |
| `pricingModel`, `hasFreeTrial` | LLM | `free` / `freemium` / `subscription` / `one-time` / `usage` / `quote` / `unknown`; trial = time-limited trial, not a free plan |
| `notableProducts`, `keywords` | LLM | Up to 5 products as named on the site; 10 lowercase keywords |
| `confidence` | LLM | 0-1, how well the pages support the record as a whole |
| `evidence` | LLM, verified | One key per judgment field: a verbatim quote (<=160 chars) or a page URL. Quotes not present in the crawled text are replaced with `null` |
| `techStackHints` | deterministic | 50 fingerprints: Shopify, WordPress, Webflow, Next.js, HubSpot, Intercom, Stripe, gtag/GTM, Segment, Meta Pixel, Greenhouse, Lever, Ashby, Cloudflare... |
| `emails`, `phones` | deterministic | From visible text and `mailto:` / `tel:` links across the fetched pages, max 10 each |
| `socials` | deterministic | linkedin, twitter/x, github, youtube, instagram, facebook (null when absent) |
| `addressHints`, `country`, `countrySource` | deterministic | JSON-LD address, then phone country code, then TLD. `countrySource` says which |
| `language` | deterministic | `<html lang>` |
| `openRolesCount` | deterministic | Distinct job links (or "N open roles" text) on the careers page; `null` when the board is JS-rendered |
| `blogLastPostDate` | deterministic | Newest date on the blog index (`<time>`, published-time meta, JSON-LD, dates in text); heuristic |
| `careersUrl`, `pricingUrl`, `contactUrl`, `aboutUrl`, `blogUrl` | deterministic | Key pages found on the homepage (external ATS boards count as careers) |
| `pagesFetched`, `pagesSkipped` | crawl | What was read; skips carry the reason (`robots.txt`, HTTP error) |
| `metaDescription`, `pageTitle` | deterministic | Raw homepage meta, kept for auditing |
| `llmModel`, `enrichedAt` | run | Which model answered, when |
| `rawText` | with `includeRawText` | The extracted text of every page, for auditing |
| `enrichment_error` | run | `null` on success; otherwise the reason and the row has no other fields |

**Industry taxonomy** (30): Software & SaaS, IT Services & Consulting, Cybersecurity, Data & Analytics, AI & Machine Learning, Developer Tools & Infrastructure, Fintech & Financial Services, Banking & Insurance, E-commerce & Retail, Consumer Goods, Marketing & Advertising, Media & Entertainment, Education & EdTech, Healthcare & Life Sciences, Biotech & Pharma, Real Estate & PropTech, Construction & Engineering, Manufacturing & Industrial, Energy & Utilities, Transportation & Logistics, Travel & Hospitality, Food & Beverage, Agriculture, Legal & Compliance, HR & Recruiting, Telecommunications, Gaming, Nonprofit & Government, Professional Services, Other.

### Input

```json
{
  "websites": ["https://www.notion.so", "linear.app", "zerodha.com"],
  "pagesToCrawl": 6,
  "llmProvider": "apify-managed",
  "model": "gpt-4o-mini",
  "includeRawText": false
}
```

| Field | Default | Notes |
|---|---|---|
| `websites` | required (or `companyNames`) | Homepage URLs or bare domains. Duplicates are removed |
| `companyNames` | `[]` | Best effort: resolves the homepage via a DuckDuckGo search for "<name> official website" and takes the first organic result. Check `website` in the row. Prefer `websites` when you have them |
| `pagesToCrawl` | 6 (max 8) | Homepage plus up to N-1 of about, pricing, careers, contact, blog, in that priority. Total text capped at 150 KB per company, 12k characters sent to the model |
| `llmProvider` | `apify-managed` | `apify-managed`: the actor's own key, LLM cost is included in the per-company price. `byok`: your key, your bill |
| `openaiApiKey`, `openaiBaseUrl`, `model` | - / `https://api.openai.com/v1` / `gpt-4o-mini` | BYOK only. Any OpenAI-compatible `/v1/chat/completions` endpoint that supports `response_format: json_object` works: OpenAI, Azure OpenAI (`https://<resource>.openai.azure.com/openai/v1`, deployment name as `model`), OpenRouter, Groq, Together, a local Ollama behind a tunnel |
| `includeRawText` | false | Attach every page's extracted text to the row (large) |
| `maxItems` | all | Stop after this many companies |
| `maxConcurrency` | 5 | Companies in parallel; pages within a company are fetched one at a time |
| `proxyConfiguration` | Apify datacenter | Use residential for sites that block datacenter IPs |

### Pricing

Pay per event:

| Event | Price |
|---|---|
| Actor start | $0.005 per run |
| Company enriched | $0.02 per successful row |
| Company failed | free |

100 companies = about $2.00. On managed mode that price includes the LLM call (roughly 3-4k input tokens and a few hundred output tokens per company on `gpt-4o-mini`). With `byok` the LLM tokens go on your own bill and the per-company price still applies. Set "Maximum total charge" on the run to cap spend; the actor stops cleanly at the cap and you keep every row already written.

### How this differs from other enrichment actors on the store

Most "company enrichment" actors return data from third-party databases (Crunchbase-style records, LinkedIn headcount, WHOIS, funding), or wrap a paid enrichment API. Those are the right tool when you need revenue, funding rounds or verified employee counts. This actor is different on purpose:

- **Source is the company's own website, read live.** No stale database, no login, no browser. Small and new companies that databases have never heard of still get a full record.
- **Judgment fields are separated from facts, and every judgment carries evidence.** You can see exactly which sentence made the model say "B2B" or "subscription", and null evidence flags inference.
- **Fixed taxonomy** for industry, business model, size band and pricing model, so the output is filterable without post-cleaning.
- **Tech, hiring and content-freshness signals** (`techStackHints`, `openRolesCount`, `blogLastPostDate`) that databases rarely have.
- **Honest on failure.** Unreachable or JS-only sites produce `enrichment_error`, not a filler row, and are not charged.

It does not return revenue, funding, founding year, verified employee counts or executive contacts. Use a database-backed actor for those.

### Limitations

- **JS-only sites.** Pages that render everything client-side give little text; the model then works from title/meta and confidence drops. Careers boards embedded as JavaScript widgets return `openRolesCount: null`.
- **`companySizeBand` is an estimate** from what the site says (team page, "N employees", customer counts, open roles). Read `sizeReasoning`. It is not a headcount from a registry.
- **`pricingModel` and `hasFreeTrial`** depend on a reachable pricing page. Sites without one usually get `quote` or `unknown`.
- **Country** is a hint (JSON-LD address, phone prefix, TLD), not the legal HQ. `.com` sites without a phone or address get `null`.
- **`blogLastPostDate`** is a heuristic over dates found on the blog index; a page that prints today's date can shift it.
- **Bot protection.** Sites behind Cloudflare challenge pages or aggressive WAFs may return 403; try residential proxies. `robots.txt` is honoured, so disallowed key pages are skipped and listed in `pagesSkipped`.
- **Company names** are resolved through a web search and can pick the wrong site for ambiguous names. Verify `website` in the row.
- One LLM call per company. Model output is schema-validated and retried once; a second invalid answer produces `enrichment_error` starting with `LLM`.

### FAQ

**Which pages does it read?** The homepage, then the about, pricing, careers, contact and blog links found on it (in that priority) up to `pagesToCrawl`. `pagesFetched` lists them per row.

**Can I trust the industry?** It is one of 30 fixed values chosen by the model from the crawled text; `evidence.industry` shows the sentence it leaned on. Where the site is a thin landing page the model still has to pick something, so check `confidence`.

**Can I use my own model?** Yes, `llmProvider: "byok"` with `openaiApiKey`, `openaiBaseUrl` and `model`. Anything OpenAI-compatible with JSON mode works. Small models are fine for this task; the prompt is short and the output is validated.

**Why is `emails` empty for a company that clearly has a contact form?** Only addresses printed on the page or in `mailto:` links are collected. Forms, images and obfuscated addresses are not decoded.

**How fast is it?** About 5-15 seconds per company (6 pages fetched sequentially plus one LLM call), 5 companies in parallel. 100 companies take roughly 5 minutes.

**Does it use a browser?** No, plain HTTP with browser-like headers, which is why it runs on 256 MB.

### Local development

```bash
npm install
## put your input in storage/key_value_stores/default/INPUT.json
export OPENAI_API_KEY=...            # managed-mode key for local runs (optional: OPENAI_BASE_URL, OPENAI_MODEL)
apify run --purge                     # or: npm start
npm test                              # offline extractor + validator checks, then a live crawl (skips offline); LLM check runs only with OPENAI_API_KEY
LLM_DRY_RUN=1 apify run --purge       # deterministic pass only, LLM fields null, rows flagged llmDryRun
ACTOR_TEST_PAY_PER_EVENT=1 ACTOR_MAX_TOTAL_CHARGE_USD=2.5 npm start   # PPE cutoff: $1 per event, stops after start + one company
```

In production the managed key is set as a secret environment variable on the actor (`OPENAI_API_KEY`, optionally `OPENAI_BASE_URL` and `OPENAI_MODEL`); it never lives in the repository.

# Actor input Schema

## `websites` (type: `array`):

One homepage URL or domain per line (https:// is added if missing). Each becomes one enriched row.

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

Names without a known website. Best effort: the homepage is resolved via a DuckDuckGo search for "<name> official website"; check the resolved URL in the output. Prefer websites\[] when you have them.

## `pagesToCrawl` (type: `integer`):

Homepage plus up to this many minus one of: about, pricing, careers, contact, blog (in that priority). robots.txt is respected.

## `maxItems` (type: `integer`):

Stop after this many companies (after dedupe). Leave empty for all.

## `includeRawText` (type: `boolean`):

Attach the extracted text of every fetched page to the row (large). Useful for auditing the LLM's answers.

## `llmProvider` (type: `string`):

Managed: the actor's own OpenAI-compatible key, LLM cost is included in the per-company price. BYOK: your key, your bill; the per-company price still applies.

## `openaiApiKey` (type: `string`):

Only used when llmProvider = byok. Works with OpenAI, Azure OpenAI (v1 endpoint), OpenRouter, Groq, Together, Ollama... anything speaking /v1/chat/completions with response\_format json\_object.

## `openaiBaseUrl` (type: `string`):

Default https://api.openai.com/v1. For Azure: https://<resource>.openai.azure.com/openai/v1. For OpenRouter: https://openrouter.ai/api/v1.

## `model` (type: `string`):

Chat model name. Must support JSON output. In managed mode the operator's default may override this.

## `maxConcurrency` (type: `integer`):

Companies processed in parallel. Each company's pages are fetched one after another.

## `proxyConfiguration` (type: `object`):

Apify datacenter proxy by default. Residential helps for sites that block datacenter IPs.

## Actor input object example

```json
{
  "websites": [
    "https://www.notion.so",
    "https://linear.app",
    "https://zerodha.com"
  ],
  "pagesToCrawl": 6,
  "includeRawText": false,
  "llmProvider": "apify-managed",
  "model": "gpt-4o-mini",
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}
```

# Actor output Schema

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

One row per company: firmographics, contacts, socials, tech hints, LLM judgments with evidence.

# 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 = {
    "websites": [
        "https://www.notion.so",
        "https://linear.app",
        "https://zerodha.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": []
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ceo.sss/ai-company-enrichment").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 = {
    "websites": [
        "https://www.notion.so",
        "https://linear.app",
        "https://zerodha.com",
    ],
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyGroups": [],
    },
}

# Run the Actor and wait for it to finish
run = client.actor("ceo.sss/ai-company-enrichment").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 '{
  "websites": [
    "https://www.notion.so",
    "https://linear.app",
    "https://zerodha.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": []
  }
}' |
apify call ceo.sss/ai-company-enrichment --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ceo.sss/ai-company-enrichment"
        }
    }
}

```

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/9QbxnLz7Sh7dTTaty/builds/TVeOflDqPWGLxeAXW/openapi.json
