# Legal Entity Resolver Domain to Company Number VAT Clay (`mambalabs/legal-entity-resolver`) Actor

Company domain in, the registered legal entity out: legal name, company number, jurisdiction, status, LEI and VAT number. Only returns a match when the legal names are identical after normalization, so an unresolved domain comes back null instead of a confidently wrong company number.

- **URL**: https://apify.com/mambalabs/legal-entity-resolver.md
- **Developed by:** [Mamba Labs](https://apify.com/mambalabs) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.95 / 1,000 domain resolveds

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/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

### 🏛️ What can Legal Entity Resolver do?

Give it a **company domain** and it returns the **registered legal entity** behind it: legal name, company number, jurisdiction, status, LEI and VAT number. One flat row per domain, 24 fields, ready for Clay or a CRM.

| 📦 What you get | ⚙️ Features and integrations |
|---|---|
| 🏛️ **Legal name and company number**, as the register spells them<br>🌍 **Jurisdiction and status**, active or dissolved, straight from the register<br>🔑 **LEI**, the 20 character global identifier<br>🧾 **VAT number**, validated live against EU VIES<br>🧾 **A full audit trail** of what was rejected and why | 🔍 **Three registers queried**: UK Companies House, GLEIF and SEC EDGAR<br>🎯 **Exact name matching only**, so a null is trustworthy<br>🧩 **Domain keyed**, no need to know the legal name first<br>⚡ **Cached 90 days** for a resolved company, 7 days for a null<br>⬇️ **Export** to JSON, CSV, Excel, HTML or XML |

Built for anyone who has a domain and needs the company behind it: KYB and onboarding, contracting and invoicing, sanctions screening, or just telling apart five similarly named companies before a deal.

> ⚠️ **A null is the product, not a gap.**
> Register search endpoints are fuzzy and they always return something. On our five company test sample, taking the top search result would have returned a **confidently wrong company number on three of them**, plus a UK subsidiary in place of its US parent and a holding group in place of the operating bank. This actor only accepts a match when the legal names are identical after normalization. That is why about 6 domains in 10 resolve instead of 10 in 10, and why the 6 are worth acting on.

> 🚫 **This is not a company database and not a credit or risk product.**
> It does not score companies, rate them, or tell you whether to trade with them. It answers one question: which registered legal entity sits behind this domain. Nothing here is an assessment of a company's health, size or standing.

### 🎯 Why use Legal Entity Resolver?

| If you need to | Read these fields |
|---|---|
| Verify who you are actually contracting with | `legal_name`, `company_number`, `jurisdiction` |
| Check the company still legally exists | `entity_status`, `entity_type` |
| Screen against sanctions or counterparty lists | `lei`, `legal_name`, `company_number` |
| Invoice a European customer correctly | `vat_number`, `vat_valid`, `vat_registered_name` |
| Prove the match was not a guess | `match_method`, `match_confidence`, `rejected_candidates` |
| Tell "not found" from "not looked for" | `registers_queried`, `register_source`, `fetch_status` |

#### 🔍 Why exact matching beats a best guess

Every register search endpoint here is fuzzy. Ask Companies House for `Stripe, LLC` and it returns 4,061 results with an unrelated company at the top. Ask GLEIF for the same string and it returns 189,994 records, with the right one ranked first purely by luck of relevance scoring.

Measured on the build sample, taking each register's top hit:

| Query | Top hit returned | Correct? |
|---|---|---|
| `Stripe, LLC` | `AALG, LLC.` | No, unrelated |
| `Swish Data Corporation` | `AZTEC DATA CORPORATION LTD` | No, unrelated |
| `AACON General Contractors` | `AACONNECTED LTD` | No, unrelated |
| `GitLab Inc.` | `GITLAB UK LIMITED` | No, the UK subsidiary |
| `Monzo Bank Limited` | `MONZO BANK HOLDING GROUP LIMITED` | No, the parent |

Five for five wrong. This actor scans the whole first page of results and accepts only an identical normalized name, so all five are rejected and recorded in `rejected_candidates` where you can see them.

### 📋 What data can Legal Entity Resolver extract?

24 fields per domain. The ones buyers actually use:

| Field | What it holds |
|---|---|
| `legal_name` | The registered legal name, spelled as the register spells it |
| `company_number` | The register's identifier, for example `09446231` |
| `company_number_scheme` | `GB-COH`, `LEI-REGISTEREDAS`, `US-SEC-CIK` or `US-STATE-FILE` |
| `jurisdiction` | ISO-2 country code |
| `entity_status` | `active`, `dissolved`, `closed`, as the register words it |
| `entity_type` | The register's own type string, for example `ltd` |
| `lei` | 20 character Legal Entity Identifier |
| `lei_status` | Whether that LEI is current |
| `vat_number` | The VAT number the company publishes on its own pages |
| `vat_valid` | EU VIES confirmation, `true`, `false` or `null` |
| `vat_registered_name` | The name VIES holds, as a cross-check on `legal_name` |
| `register_source` | Which register matched |
| `registers_queried` | Which registers were asked |
| `match_method` | `exact_normalized`, `input_hint_exact`, `fuzzy_scored` or `none` |
| `match_confidence` | `100` for exact, `40` to `90` for fuzzy, `null` for no match |
| `legal_name_candidates` | Every name read off the site, with its source |
| `rejected_candidates` | Every near miss thrown away, and why |
| `pages_attempted`, `pages_reached` | How much of the site was readable |
| `fetch_status`, `fetch_error` | What happened |

> ⚠️ **`legal_name` populated with `company_number` null is a real answer, not a failure.**
> It means the legal name was read off the company's own site but no register we can query for free holds it. Small US corporations are the common case: most have no LEI and are registered at state level, which has no free national search. `fetch_status` will read `no_register_match`. Throwing that row away loses the half that worked.

### 🚀 How to find the registered company behind a domain

1. Open the **Input** tab and put a single domain in `domain`, for example `monzo.com`. Protocol and path are stripped.
2. Leave `match_strictness` on `exact`. This is the setting that makes a null trustworthy.
3. Leave `validate_vat` on to check any VAT number found against EU VIES.
4. Run it. One flat row comes back per domain.
5. Read `fetch_status` first, then `company_number`. If `fetch_status` is `no_register_match`, `legal_name` is still populated and still useful.

#### 🧩 How to use it in Clay

Add it as an enrichment on a company table, map your domain column to `domain`, and pull `legal_name`, `company_number`, `jurisdiction` and `entity_status` into columns. Every field is flat `snake_case` with `null` rather than a missing key, so no unnesting is needed.

If you already have the legal name and only want the register record, put it in `legal_name_hint`. That skips the page fetch entirely and goes straight to the registers.

#### 🌍 How to narrow it to one country

Set `jurisdiction_hint` to an ISO-2 code. `GB` asks Companies House and GLEIF. `US` asks GLEIF and SEC EDGAR. Anything else asks GLEIF. It cuts latency, and `registers_queried` records which registers were actually asked so a null is never ambiguous.

### 💰 How much does it cost to resolve a domain?

| Plan | Per domain |
|---|---|
| Free | $0.007 |
| Bronze | $0.00665 |
| Silver | $0.0063 |
| Gold | $0.00595 |

> 💳 **Every domain is billed, including the ones that come back null.**
> The two hop lookup runs either way. Charging only for matches would push this actor toward guessing, which is the exact failure it is built to avoid. The one thing never billed is a domain that could not be reached at all.

### ⌨️ Input

Set everything on the **Input** tab. Only `domain` is required.

| Field | Default | What it does |
|---|---|---|
| `domain` | required | One company domain, for example `monzo.com` |
| `legal_name_hint` | empty | Skips the page fetch and queries the registers with this name |
| `jurisdiction_hint` | empty | ISO-2 code. Narrows which registers are asked |
| `match_strictness` | `exact` | `exact` or `fuzzy`. See the warning below |
| `validate_vat` | `true` | Checks any VAT number found against EU VIES |
| `skipCache` | `false` | `true` forces a fresh lookup |

> ⚠️ **`fuzzy` will hand you the wrong company on most domains.**
> It exists because some buyers deliberately want the near miss. It caps `match_confidence` at 90 and writes a warning into `rejected_candidates` naming what exact matching would have returned instead, which on our sample was usually nothing. Leave it on `exact`.

### 📤 Output

Export to **JSON, CSV, Excel, HTML or XML**, or pull it straight from the API. One row per domain.

```json
{
  "domain": "monzo.com",
  "legal_name": "MONZO BANK LIMITED",
  "company_number": "09446231",
  "company_number_scheme": "GB-COH",
  "jurisdiction": "GB",
  "entity_status": "active",
  "entity_type": "ltd",
  "lei": "213800PLUYXGZ8LU7T61",
  "lei_status": "ISSUED",
  "vat_number": null,
  "vat_valid": null,
  "vat_registered_name": null,
  "register_source": "companies_house,gleif",
  "registers_queried": "companies_house,gleif,sec_edgar",
  "match_method": "exact_normalized",
  "match_confidence": 100,
  "legal_name_source": "legal_page",
  "rejected_candidates": "[{\"name\":\"MONZO BANK HOLDING GROUP LIMITED\",\"register\":\"gleif\",\"reason\":\"name mismatch under exact normalized matching\"}]",
  "pages_attempted": 9,
  "pages_reached": 4,
  "fetch_status": "ok",
  "fetch_error": null,
  "run_date": "2026-08-10T09:12:04.118Z"
}
```

### 💡 Tips

- **Read `fetch_status` before you read `company_number`.** It tells you whether a null means "not in the register" or "we could not look".
- **`rejected_candidates` is worth reading on any row that surprises you.** It shows the answer a less careful tool would have given.
- **A company's own legal pages name other companies.** Its subsidiaries, its parent, its credit reference agencies. Those are found, matched and then rejected as a different company, and they appear in `rejected_candidates`.
- **Batch through Clay one domain per row.** The cache holds a resolved company for 90 days and a null for only 7, so re-running a batch is cheap and newly registered companies are picked up quickly.
- **German, Austrian and Dutch companies almost always have a VAT number.** US companies never do, because there is no equivalent.

### ⚠️ Known limits

- **About 6 domains in 10 resolve to a register record.** Measured on real companies spanning US enterprise, UK regulated, mid-market and a small contractor.
- **The US SEC register is currently unreachable.** `sec.gov` returns HTTP 403 to automated requests from our infrastructure, measured at **7 of 7 runs on 2026-08-10**. SEC EDGAR is only ever a corroborator here and can never be the sole source of a company number, so US companies still resolve through GLEIF when they hold an LEI. What it means for you: a US company with no LEI that would have been corroborated by an SEC filing comes back `null` instead. It is still queried on every row, and `registers_queried` and `fetch_error` record the outcome.
- **Coverage is UK, US and LEI holders.** Companies House covers all UK registered companies. GLEIF covers any entity worldwide holding an LEI, which skews to financial counterparties and larger firms. There is no free national register API for most countries, and no US federal company register at all.
- **A subsidiary is not the parent and we will not swap one for the other.** Both were returned by register searches on the build sample and both were rejected. `rejected_candidates` shows what was set aside.
- **`company_number_scheme` of `LEI-REGISTEREDAS` is not always a company registration number.** It is the identifier the entity's home register holds, and for a regulated firm that can be the financial regulator's reference. Measured: GLEIF returns `730427` for one UK bank, which is its FCA firm reference, not its Companies House number. When Companies House answers, `GB-COH` is used and that ambiguity does not arise.
- **VAT numbers only come from what the company publishes.** There is no way to search for a company's VAT number by name. The EU service can only validate a number you already have, so we read it from terms, legal and Impressum pages.
- **UK lookups are rate limited to 600 requests every five minutes.** On a large batch some rows come back with `fetch_status: register_error` rather than a match. Re-run those rows; the cache skips the ones that already resolved.
- **Some sites block a plain fetch.** Requests carry full browser headers and `robots.txt` is honored. When a site blocks us anyway, `pages_reached` is lower than `pages_attempted` and the row says so.
- **`fetch_status: partial` means it resolved and something that could have improved the answer failed.** Read `fetch_error`. A row can be `ok` and still carry a `fetch_error` when the thing that failed could not have changed the answer.

### ❓ FAQ

**Why does it return null so often?**
Because the alternative is worse. Every register search is fuzzy and always returns something, so a tool that always answers is a tool that is often wrong. See the table above: five for five wrong on top hits.

**Can I use it if I already know the legal name?**
Yes. Put it in `legal_name_hint` and it skips straight to the registers.

**Does it return company directors or owners?**
No, and it never will. This actor names companies, not people. There are no directors, no officers and no persons of significant control anywhere in the output.

**Which registers does it query?**
UK Companies House, GLEIF and SEC EDGAR. `registers_queried` tells you which were asked on each row and `register_source` tells you which answered.

**Is the data licensed for commercial use?**
Yes. See the sourcing note at the bottom.

**How fresh is a cached row?**
A resolved company is cached for 90 days, a null for 7. Pass `skipCache: "true"` to force a fresh lookup.

### 🧩 Want other GTM data?

Mamba Labs builds custom actors for B2B go-to-market teams. The public versions
of that work live here on the Store, so our users get the same tooling we build
under contract.

| | |
|---|---|
| 🧑‍💼 [GTM Hiring Signal Scraper](https://apify.com/mambalabs/gtm-hiring-signal-scraper) | 🧱 [Tech Stack Detector](https://apify.com/mambalabs/gtm-tech-stack-signal-scraper) |
| 📡 [B2B Buying Signals Aggregator](https://apify.com/mambalabs/b2b-buying-signals-hiring-tech-stack-intent-for-clay) | 🔑 [Job Board Keyword Scanner](https://apify.com/mambalabs/job-board-keyword-signal-scanner) |
| 🔗 [Domain to LinkedIn URL Resolver](https://apify.com/mambalabs/domain-to-linkedin-url-resolver) | 🎯 [ICP Fit Scorer](https://apify.com/mambalabs/icp-account-lead-scoring-fit-scorer-0-100-for-clay) |
| 📋 [Job Posting Monitor](https://apify.com/mambalabs/gtm-job-discovery) | 📬 [Domain Deliverability Checker](https://apify.com/mambalabs/domain-deliverability-checker) |
| 🏢 [Company Firmographic Enricher](https://apify.com/mambalabs/company-firmographic-enricher) | 🌐 [Company Social Presence Mapper](https://apify.com/mambalabs/company-social-presence-mapper) |
| 🪪 [Company Identity Resolver](https://apify.com/mambalabs/company-identity-resolver) | 💰 [Funding and Press Signal Scanner](https://apify.com/mambalabs/funding-press-signal-scanner) |
| 🔄 [Company Change-Event Feed](https://apify.com/mambalabs/company-change-event-feed) | 👤 [People Finder and Email Verifier](https://apify.com/mambalabs/people-finder) |
| 🚀 [Prospect Engine](https://apify.com/mambalabs/b2b-prospect-engine) | 🤖 [AI Tooling Detector](https://apify.com/mambalabs/ai-tooling-detector) |
| 📮 [Outbound Stack Detector](https://apify.com/mambalabs/outbound-infrastructure-fingerprint) | 📝 [Publishing Frequency Tracker](https://apify.com/mambalabs/blog-publishing-frequency) |
| ✉️ [Work Email Waterfall Finder](https://apify.com/mambalabs/email-waterfall-orchestrator) | ⏩ [Sequencer Lead Push](https://apify.com/mambalabs/clay-to-instantly-smartlead-push) |
| 🏅 [Workplace Program Detector](https://apify.com/mambalabs/workplace-program-detector) | 👥 [Team Page People Extractor](https://apify.com/mambalabs/team-page-people-extractor) |
| 🧭 [Company Discovery List Builder](https://apify.com/mambalabs/company-discovery-list-builder) |  |

> Every actor in the suite takes a domain or a company and returns one flat row,
> so they stack in the same Clay table without reshaping anything.

> 🛠️ **Need something custom built for you or your team?** Tell us what you are
> trying to find and we will build it. [Talk to Mamba Labs](https://mambabuilt.com/contact).

### 🛟 Support

Something wrong, or a domain that should resolve and does not? Open an issue on the **Issues** tab with the domain and the row you got back. `rejected_candidates` and `registers_queried` are the two fields that make a report easy to act on.

> **Sourcing and licensing.** Contains public sector information licensed under the Open Government Licence v3.0. Company data comes from UK Companies House, GLEIF (published under CC0), SEC EDGAR, the EU VIES VAT validation service, and the companies' own public web pages. This actor returns registered company information only. It contains no personal data, no directors and no beneficial owners, and it is not an assessment of any company's creditworthiness, risk or standing.

Built by [Mamba Labs](https://apify.com/mambalabs).

# Actor input Schema

## `domain` (type: `string`):

A single company domain, for example monzo.com. Protocol and path are stripped.

## `legal_name_hint` (type: `string`):

Skips the domain lookup and goes straight to the registers with this name. Use it when you already have the legal name and just want the register record.

## `jurisdiction_hint` (type: `string`):

ISO-2 country code, for example GB or US. Narrows which registers are queried and cuts latency. Leave empty to query every register.

## `match_strictness` (type: `string`):

exact accepts a register record only when the normalized legal names are equal, which is the default and the recommendation. fuzzy returns the best scoring candidate with a confidence below 100 and a warning in rejected\_candidates. Register search is fuzzy and always returns something, so fuzzy mode will hand you a confidently wrong company on most domains.

## `validate_vat` (type: `boolean`):

Runs any VAT number found on the company's own pages through the EU VIES service and returns the name VIES holds for it, as a cross-check against the register name.

## `skipCache` (type: `string`):

false uses the cache: 90 days for a resolved company, 7 days for a null. true forces a fresh look.

## Actor input object example

```json
{
  "domain": "monzo.com",
  "legal_name_hint": "Monzo Bank Limited",
  "jurisdiction_hint": "GB",
  "match_strictness": "exact",
  "validate_vat": true,
  "skipCache": "false"
}
```

# Actor output Schema

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

No description

# 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 = {
    "domain": "monzo.com",
    "match_strictness": "exact",
    "validate_vat": true,
    "skipCache": "false"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mambalabs/legal-entity-resolver").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 = {
    "domain": "monzo.com",
    "match_strictness": "exact",
    "validate_vat": True,
    "skipCache": "false",
}

# Run the Actor and wait for it to finish
run = client.actor("mambalabs/legal-entity-resolver").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 '{
  "domain": "monzo.com",
  "match_strictness": "exact",
  "validate_vat": true,
  "skipCache": "false"
}' |
apify call mambalabs/legal-entity-resolver --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mambalabs/legal-entity-resolver"
        }
    }
}

```

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/KHFyPCDIx7CyqULYm/builds/lcVyuZRNw4uFGpNNP/openapi.json
