# Brazil CNPJ Lookup & Enrichment (Batch) (`jgoia/cnpj-lookup`) Actor

Batch-lookup Brazilian company registry (CNPJ) records: legal name, status, address, CNAE activity codes, partners, and more. Multi-source failover, pay only for resolved records.

- **URL**: https://apify.com/jgoia/cnpj-lookup.md
- **Developed by:** [Alison Moura](https://apify.com/jgoia) (community)
- **Categories:** Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 resolved cnpj records

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

## Brazil CNPJ Lookup & Enrichment (Batch)

Batch-lookup Brazilian company registry (CNPJ) records and get back clean,
normalized JSON: legal name, trade name, registration status, full
address, primary/secondary CNAE activity codes, partners (sócios), Simples
Nacional/MEI status, and more — one record per CNPJ, ready to pipe into a
CRM, KYC/compliance check, lead-scoring pipeline, or sales database.

### Who this is for

Anyone building on top of Brazilian company data without wanting to deal
with Receita Federal's raw, denormalized open-data dumps directly: fintech/
KYC and AML tooling, B2B sales and lead-gen tools that need to validate and
enrich a list of company CNPJs, compliance workflows, procurement/vendor
vetting, or just a developer who needs "give me company X's registered
address and legal status" without writing a CNPJ parser from scratch.

### What you send in

```json
{
  "cnpjs": [
    "33.000.167/0001-01",
    "19191919000138"
  ],
  "maxConcurrency": 5
}
```

- `cnpjs` — a list of CNPJs, punctuation optional (`33.000.167/0001-01` and
  `33000167000101` both work). Supports both the classic all-numeric CNPJ
  and the new **alphanumeric CNPJ** format Receita Federal began issuing to
  new registrations on 2026-07-31 (Instrução Normativa RFB nº 2229/2024) —
  existing numeric CNPJs are unaffected and keep working forever, the two
  formats simply coexist.
- `maxConcurrency` — optional, default 5, max 10. Kept modest on purpose;
  see "Rate limits & fair use" below.

### What you get back

One dataset record per input CNPJ. Example (trimmed for length — the real
output includes all 8 Petrobras partners and all 5 secondary CNAE codes):

```json
{
  "cnpj": "33000167000101",
  "cnpj_formatted": "33.000.167/0001-01",
  "razao_social": "PETROLEO BRASILEIRO S A PETROBRAS",
  "nome_fantasia": "PETROBRAS - EDISE",
  "status": { "code": 2, "description": "ATIVA", "reason": "SEM MOTIVO", "date": "2005-11-03" },
  "opening_date": "1966-09-28",
  "legal_nature": { "code": 2038, "description": "Sociedade de Economia Mista" },
  "company_size": { "code": 5, "description": "DEMAIS" },
  "share_capital_brl": 205431960000,
  "branch": { "is_headquarters": true, "type_code": 1, "type_description": "MATRIZ" },
  "primary_activity": { "code": "0600001", "description": "Extração de petróleo e gás natural" },
  "secondary_activities": [
    { "code": "1921700", "description": "Fabricação de produtos do refino de petróleo" }
  ],
  "address": {
    "street_type": "AVENIDA", "street": "REPUBLICA DO CHILE", "number": "65",
    "complement": null, "neighborhood": "CENTRO", "city": "RIO DE JANEIRO",
    "city_ibge_code": 3304557, "state": "RJ", "zip_code": "20031170", "country": "Brasil"
  },
  "contact": { "phones": ["2121660000"], "fax": "213224", "email": null },
  "simples_nacional": { "opted_in": null, "opted_in_date": null, "excluded_date": null },
  "mei": { "opted_in": null, "opted_in_date": null, "excluded_date": null },
  "partners": [
    { "name": "ANGELICA GARCIA COBAS LAUREANO", "role": "Diretor", "role_code": 10,
      "entry_date": "2025-07-29", "age_range": "Entre 71 a 80 anos",
      "document_masked": "***912137**", "legal_representative_name": null }
  ],
  "partners_count": 8,
  "_meta": { "source": "minhareceita", "retrieved_at": "2026-08-17T13:45:00+00:00" }
}
```

For a CNPJ that's invalid, not found, or that every source failed to
answer, you still get a record so you can see what happened to every
input — it's just not charged (see Pricing):

```json
{ "cnpj_input": "not-a-cnpj", "status": "invalid_format", "error": "expected 14 characters after removing punctuation, got 8" }
{ "cnpj": "10433218196071", "cnpj_formatted": "10.433.218/1960-71", "status": "not_found" }
```

Partner CPFs are pre-masked by the upstream source (e.g. `***912137**`) —
Receita Federal's own public data never exposes full partner CPFs through
these APIs, so there's no additional PII handling risk here.

### Pricing

Pay-per-event: **you are charged only for successfully resolved CNPJs**
(event `cnpj-record`). Invalid CNPJ formats, not-found CNPJs, and CNPJs
where every source failed are written to your dataset for visibility but
are never charged. See the Actor's Store page for the current per-event
price.

### Data sources, freshness & accuracy

Underlying data is Brazil's official CNPJ company registry, published by
Receita Federal (the federal tax authority) as free, public open data.
Neither this Actor nor its upstream sources guarantee the data is
real-time or free of errors — Receita Federal's own open-data dumps are
refreshed periodically (commonly monthly), and any of the CNPJ record
fields (status, address, capital, etc.) can lag a company's real-world
state by that update cycle. For anything with legal/compliance weight,
treat this as a fast first-pass lookup, not a substitute for an official
Receita Federal certidão.

### Rate limits & fair use

This Actor deliberately paces its own requests to the free upstream APIs
it wraps (see "Design decision" below) rather than firing everything at
once — neither upstream source publishes a fixed requests/second limit,
but both are free community services without an SLA, and hammering them
would be a good way to get blocked and break this Actor for everyone
using it. In practice this means: very large batches (hundreds+ of CNPJs)
will take some real wall-clock time to finish, by design. `maxConcurrency`
lets you tune this within a conservative ceiling (max 10).

***

### For the Aurora project (internal)

This product was built as Aurora's first shippable revenue attempt (see
the repo root `README.md`, `AGENTS.md`, `OPPORTUNITIES.md` #1, and
`DECISIONS.md` ADR-003/ADR-004 for the full strategic context). This
section is the build record for that decision chain — deployment is the
one remaining owner action.

#### Design decision: wrap free public APIs (option A), not self-host (option B)

**Chose option (a)**: wrap existing free public CNPJ lookup APIs with
batching/failover/retry, rather than (b) downloading and self-hosting a
queryable index of the full Receita Federal open-data dump on this VPS.

Reasoning:

- The full RFB CNPJ open-data dump lives at
  `dadosabertos.rfb.gov.br/CNPJ/dados_abertos_cnpj/`, published monthly as
  zipped CSV files split into `Empresas*.zip`, `Estabelecimentos*.zip`,
  `Socios*.zip`, plus reference tables (CNAEs, municípios, etc). This VPS
  could not reach the domain directly during this research session
  (connection timed out on both an HTTPS fetch and a raw `curl` from this
  machine — possibly transient, possibly network-level filtering; not
  investigated further since it wasn't blocking for the option-(a) path),
  so this is based on Receita Federal's own gov.br documentation page and
  the READMEs of the two community parser projects named in ADR-004
  (`cuducos/minha-receita`, `aphonsoar/...Dados_Publicos_CNPJ`), not a
  direct fetch. Reported size as of 2021 was ~4.7GB compressed / ~17GB
  uncompressed; five more years of company registrations since then means
  the current size is almost certainly larger, and a queryable index
  (as opposed to raw CSVs) typically adds overhead on top of that. This
  VPS has 3.8GB RAM and 77GB disk total — self-hosting the *full* dataset
  would consume a large share of the disk budget and leave little
  headroom for anything else Aurora might need to run concurrently, for a
  V1 whose demand is still unvalidated.
- Option (a) needs no VPS-uptime dependency for the Actor to function (it
  calls the source APIs directly from Apify's infrastructure, not from
  this VPS), ships in hours not days, and lets us learn whether this
  product gets real paid usage before investing in self-hosting — the
  exact sequencing ADR-003/004 already committed to ("ship one actor,
  measure 1-2 weeks, only then invest more").
- Nothing found during source research (below) made option (a) infeasible
  — the opposite of the trigger condition the task set for falling back
  to (b).

If this Actor gets real, sustained paid usage and the free public sources
turn out to be a reliability bottleneck, the natural next step (not done
here) is exactly option (b): self-host a `minha-receita`-style index built
from the same RFB open data minhareceita.org itself is built from, sized
down to just the fields this Actor's schema uses to fit VPS RAM/disk
constraints, and have this Actor's `sources.py` call `http://127.0.0.1`
instead of the public internet. The self-hosted instance would slot in as
a new highest-priority `SourceConfig` in `sources.py` with no change to
`normalize.py`, since it would speak the same RFB field schema.

#### Source selection (verified 2026-08-17, not assumed)

Three free public CNPJ APIs were evaluated: BrasilAPI, ReceitaWS, and
Minha Receita.

| Source | Used as | Why |
|---|---|---|
| **minhareceita.org** | Primary | MIT-licensed, fully open source, explicitly built to be self-hostable ("I make this application available for each person to run in their own infrastructure"). No fixed published rate limit and no SLA, but also **no clause anywhere restricting commercial use or resale** — the strongest permissive signal of the three. |
| **brasilapi.com.br** | Failover only | Verified byte-identical response schema to minhareceita for the same CNPJ (see `tests/fixtures/*_petrobras.json` — same field names, same "não encontrado" 404 body), meaning it's backed by the same RFB data pipeline: it adds *operational* independence (separate infra/caching) if minhareceita.org is down, but not *data* independence. Its own Terms of Use are still in beta and ambiguous about bulk/automated use, so it's deliberately never the primary source here, and is paced even more conservatively than minhareceita (1 req/s self-imposed ceiling vs 2 req/s). |
| **receitaws.com.br** | **Not used, anywhere** | Its Terms of Use (`receitaws.com.br/terms.pdf`, Cláusula Oitava, fetched and read in full) state verbatim: *"Os dados retornados através das APIs podem ser utilizados apenas para benefício próprio do usuário, não sendo permitida qualquer comercialização através de outros sites ou qualquer outro meio"* — data returned may only be used for the user's own benefit; commercialization through other sites/means is explicitly forbidden. That's a direct conflict with reselling it as a paid Actor, so it was excluded per this task's own instruction to drop any source whose terms explicitly prohibit resale. (Its free tier is also capped at 3 requests/minute, which alone would make it impractical for a batch product.) |

Both used sources are paced with self-imposed, deliberately conservative
request intervals (not taken from a published limit — neither source
publishes one) plus exponential backoff on 429/5xx and automatic failover
between them; see the module docstring in `cnpj_lookup/sources.py` for the
full reasoning and exact ToS quotes.

#### What was actually tested vs. not

**Tested and passing:**

- `cnpj_lookup/cnpj_utils.py` — CNPJ cleaning, both classic-numeric and
  new-alphanumeric check-digit validation, formatting. The alphanumeric
  check-digit algorithm was independently re-derived from Receita
  Federal's published rule (ASCII-48 char mapping + mod-11, same weight
  sequences as the classic algorithm) and cross-checked against a
  third-party worked example, which it reproduces exactly (see
  `tests/test_cnpj_utils.py` docstring).
- `cnpj_lookup/normalize.py` — mapping tested against **real** API
  responses (not hand-written fixtures) captured 2026-08-17 for CNPJ
  33.000.167/0001-01 (Petrobras, a public real-world CNPJ) from both live
  sources.
- `cnpj_lookup/sources.py` — failover, not-found short-circuiting, retry/
  backoff, and rate-limiting logic, tested with `httpx.MockTransport`
  (no real network calls in the automated test suite).
- 31/31 tests pass (`pytest`, run inside a Python 3.12 venv — see below on
  why 3.12 not the VPS's default 3.10).
- **Full end-to-end run against the live internet**, via both a direct
  `python -m cnpj_lookup` invocation and the official `apify-cli run`
  command (which reads `.actor/actor.json` exactly as the real platform
  would): verified all three real code paths live — a real resolved CNPJ
  (Petrobras), a real not-found CNPJ (`10.433.218/1960-71`, a randomly
  generated but checksum-valid CNPJ confirmed via live lookup to not
  exist in the registry), and a real invalid-format input. Deduping a
  CNPJ supplied in two different formats was also verified live (both
  `"33.000.167/0001-01"` and `"33000167000101"` in one input list produced
  exactly one output record).
- `apify-cli validate-schema` passes for both `input_schema.json` and
  `dataset_schema.json`.
- Ruff lint: clean, zero warnings.

**Not tested / could not verify locally:**

- The "all sources failed" path (`AllSourcesFailedError`) is only
  exercised by the mocked unit tests, not live — there was no way to make
  both real upstream APIs fail simultaneously on demand.
- Real pay-per-event **charging** behavior: running locally, the Apify
  SDK correctly no-ops `Actor.charge`/`push_data(charged_event_name=...)`
  with the log line `"Ignored attempt to charge for an event - the Actor
  does not use the pay-per-event pricing"`, because no Apify platform
  pricing config is available outside a real platform run. The charging
  *code path* is exercised (it's called on every successful resolution)
  but the actual billing behavior can only be confirmed after deploying
  and monetizing on the real platform.
- **Docker build**: this VPS has no Docker installed (`docker` is not on
  PATH, no root/sudo available to install it), so the `Dockerfile` was
  never actually built into an image here. It follows Apify's own current
  official Python Actor template (`apify/actor-templates`,
  `templates/python-start/Dockerfile`, fetched via `gh api` 2026-08-17)
  line-for-line except for the base image tag (3.13, chosen for stability
  — some Apify example templates note 3.14 has known incompatibilities
  with certain compiled dependencies) and the package name
  (`cnpj_lookup` instead of `my_actor`). Apify's own build pipeline will
  build this Dockerfile when you `apify push` or upload via Console — that
  step has not happened yet (see Deployment below).
- Local testing used **Python 3.12** in a throwaway venv, not the VPS's
  default Python 3.10.12 — the `apify` SDK requires Python ≥3.11, and this
  VPS's `python3.10-venv` isn't installed and there's no passwordless sudo
  to install it (a known constraint, see `AGENTS.md`). Python 3.12 was
  available system-wide (`python3.12`, with a working `venv` module) and
  was used instead; this matches what the Actor will actually run under
  on Apify's platform (3.13 per the Dockerfile) closely enough not to be
  a concern, since the SDK's 3.11+ requirement is the load-bearing
  constraint, not the exact minor version.
- No real paid usage, no Apify Store listing yet — this Actor has not
  been deployed anywhere outside this repo and this VPS.

#### Deployment (the one remaining owner action)

This Actor is built, tested (as above), and ready to push — it has not
been published. Publishing requires an Apify account, which needs a
one-time human browser step (OAuth consent) that's out of scope for
Aurora to do autonomously (see ADR-004). Steps for the owner:

1. **Create an Apify account**, if not already done: go to
   [apify.com](https://apify.com), "Continue with GitHub" (should reuse
   `alison-moura`'s already-verified GitHub email, no new email
   verification loop expected).
2. **Install the Apify CLI** (already verified working via `npx
   apify-cli` in this repo — no separate install needed) and log in:
   ```bash
   npx apify-cli login
   ```
3. **Push the Actor** from this directory:
   ```bash
   cd products/cnpj-lookup
   npx apify-cli push
   ```
   This builds the Docker image on Apify's infrastructure (not locally)
   and creates/updates the Actor under your account.
4. **Set up monetization** in Apify Console (Actor → Monetization): choose
   "Pay per event", and configure the `cnpj-record` event (title,
   description, and price are already defined in
   `.actor/pay_per_event.json` — the Console may pick these up
   automatically on push, or may require re-entering them manually; this
   step was not verifiable without a live account). A price of $0.01–0.03
   per resolved record is a reasonable starting point given Casa dos
   Dados (the existing commercial competitor named in ADR-004) charges
   for the same underlying data.
5. **Write/attach a payout method** (PayPal, $20 minimum payout — fastest
   path per ADR-003's own research) so revenue can actually be withdrawn.
6. **Publish to the Apify Store** (Actor → Publication) so it's
   discoverable — this Actor's `README.md` (this file, the part above the
   "For the Aurora project" section) is written for that Store listing.
7. Report back actual results (impressions, runs, revenue) into
   `BUSINESS.md` — do not fabricate numbers before they exist.

No code changes should be needed for any of the above; this is a
Console/account-setup sequence, not a development task.

# Actor input Schema

## `cnpjs` (type: `array`):

List of CNPJs, one per line/item. Punctuation (dots, slash, dash) is optional - both "33.000.167/0001-01" and "33000167000101" work. Supports both classic all-numeric CNPJs and the new alphanumeric CNPJ format (rolled out by Receita Federal starting July 2026).

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

How many CNPJs to look up in parallel. Kept low by default to stay well within the free source APIs' unpublished but real rate limits. Max 10.

## Actor input object example

```json
{
  "cnpjs": [
    "33.000.167/0001-01"
  ],
  "maxConcurrency": 5
}
```

# 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 = {
    "cnpjs": [
        "33.000.167/0001-01"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jgoia/cnpj-lookup").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 = { "cnpjs": ["33.000.167/0001-01"] }

# Run the Actor and wait for it to finish
run = client.actor("jgoia/cnpj-lookup").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 '{
  "cnpjs": [
    "33.000.167/0001-01"
  ]
}' |
apify call jgoia/cnpj-lookup --silent --output-dataset

```

## MCP server setup

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

```

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/gOcd9636y7eejuoC5/builds/8wR1zBtJkQKjNMmGD/openapi.json
