By the end of this tutorial, you will have a short Python script that turns a company-name search into structured Brazilian company records: CNPJ, legal name (razão social), trade name (nome fantasia), registration status, primary CNAE activity, share capital, size tier and address.
Every Brazilian company has a CNPJ, the 14-digit national registry number. If you build B2B lead lists or check suppliers, you usually want those records filtered by state, status and activity, not looked up one by one.
In this guide, we use Python and the Apify Actor CNPJ Brasil Scraper (reventadata/cnpj-brasil-scraper). The sample rows below come from a local test run dated 2026-10-24, made with the input shown in the code. The Actor is unofficial and not affiliated with Casa dos Dados or Receita Federal.
⏱️ Time estimate: one sitting: install one package, set one token, run three short Python blocks.
Prerequisites
To follow this tutorial, you will need:
- Python 3.11+ (
apify-client3.3.0 requires it). - The Apify API client for Python.
- An Apify account and its API token, which sits on the API & Integrations page in Apify Console.
Install the client:
pip install -U "apify-client~=3.0" The code reads the token from an environment variable:
export APIFY_API_TOKEN="your_apify_token_here" What you'll achieve
You will turn a keyword search into one row per company. Each row carries cnpj and cnpjFormatted, legalName, tradeName (null when none is registered in the data), status, openingDate, cnaeCode and cnaeDescription, shareCapital in reais, legalNature, companySize, the isSimplesNacional, isMei and isMatriz flags, and the address split into city, state, neighborhood, street and zipCode.
Note: rows hold company fields only. The row has no partner list, CPF, phone or e-mail field, and the name search does not look at partner names.
Example use case: Suppose you want active software firms headquartered in Paraná. You would set searchQueries: ["software"], state: "PR", status: "ATIVA", cnaeCode: "6201501" and onlyMatriz: true, and every saved row would be an active company registered in PR whose name contains "software", found by a search limited to headquarters with that primary CNAE.
Step 1: Access CNPJ Brasil Scraper
Go to the CNPJ Brasil Scraper page on Apify Store. The README lists every input and output field. In this tutorial we drive the Actor from Python, so everything after this step happens in code.
Step 2: Configure the search
The main input is searchQueries: one search per keyword, matched against the legal name and the trade name. A company is kept only if every word of the keyword appears in the two names taken together, ignoring case and accents, so seguranca eletronica also matches SEGURANÇA ELETRÔNICA.
Two inputs set the scope:
-
state: a two-letter code (SP,RJ,MG,PR, ...) orALLfor nationwide. DefaultSP. -
status:ATIVA(active),BAIXADA(closed) orALL. DefaultATIVA.
maxItems (1 to 5,000, default 50) caps the rows saved across all keywords.
Here is the setup. It searches tecnologia among active companies in São Paulo state and saves at most 5 rows, the same input as the sample run:
import os from apify_client import ApifyClient # 1. Initialize ApifyClient with the token from the environment client = ApifyClient(os.environ["APIFY_API_TOKEN"]) # 2. Define the search: active companies in SP whose name contains "tecnologia" run_input = { "searchQueries": ["tecnologia"], "state": "SP", "status": "ATIVA", "maxItems": 5, } 💡 Tip: a keyword matches names, not activities. To find companies by what they do, combine a short keyword with cnaeCode (Step 3).
No proxy setting is needed: by default the Actor connects directly, and a direct connection returned real rows in the test run on 2026-10-24.
Step 3: Add optional filters
These inputs go into the search itself, and state, status and capital are checked again on each finished row:
| Parameter | Type | What it keeps |
|---|---|---|
minCapital / maxCapital | integer | Companies whose registered share capital in reais is inside the band. Rows without a capital figure are dropped when either is set. |
cnaeCode | string | Companies whose primary activity is this 7-digit CNAE code, for example 6201501 (custom software development). Non-digits are stripped, so 6201-5/01 works too. |
onlyMatriz | boolean | Headquarters only; branches (filiais) are left out. |
Three combinations that work:
- Established IT firms in Rio:
searchQueries: ["tecnologia"],state: "RJ",minCapital: 100000. - Closed logistics companies nationwide:
searchQueries: ["logistica"],state: "ALL",status: "BAIXADA". - Software headquarters in Minas Gerais:
searchQueries: ["sistemas"],state: "MG",cnaeCode: "6201501",onlyMatriz: true.
⚠️
stateandstatusonly accept the listed values; anything else is refused before the run starts. Maranhão (MA) is not in the state list: useALLand filterstatein your own code.
Step 4: Run the Actor
Now start the run with client.actor("reventadata/cnpj-brasil-scraper").call(run_input=run_input). The call starts the Actor on Apify and waits for it to finish before returning the run:
# 3. Start the Actor run and wait for it to finish print("Starting CNPJ search...") run = client.actor("reventadata/cnpj-brasil-scraper").call(run_input=run_input) if run is None or run.status != "SUCCEEDED": status = run.status if run else "unknown" raise SystemExit(f"Run did not succeed (status: {status}).") # 4. Fetch the saved company records items = client.dataset(run.default_dataset_id).list_items().items print(f"Retrieved {len(items)} companies.\n") Behind the scenes:
- The Actor sends your keyword and filters to Casa dos Dados' public company search and reads the results in pages of 20.
- For each match, it looks up the CNPJ's registry details on minhareceita.org: opening date, CNAE, capital, size, tax flags and address.
- It drops companies already saved under an earlier keyword, applies the checks, and stops at
maxItemsor when the search runs dry.
Step 5: View and download results
Loop over the rows to print the parts you need:
for item in items: print(f"{item['cnpjFormatted']} {item['legalName']}") print(f" Trade name: {item.get('tradeName') or 'none'}") print(f" CNAE: {item.get('cnaeCode')} {item.get('cnaeDescription')}") print(f" City: {item.get('city')}/{item.get('state')}") print(f" Share capital (BRL): {item.get('shareCapital')}") print("-" * 40) Here is one row from the local test run dated 2026-10-24, exactly as the Actor saved it:
{ "cnpj": "97552676000120", "cnpjFormatted": "97.552.676/0001-20", "legalName": "H&M TECNOLOGIA DA INFORMACAO LTDA", "tradeName": "MASSIMUS CONSULTING AND TRAINING", "status": "ATIVA", "openingDate": "2011-07-05", "cnaeCode": "8599603", "cnaeDescription": "Treinamento em informática", "shareCapital": 20000.0, "legalNature": "Sociedade Empresária Limitada", "companySize": "MICRO EMPRESA", "isSimplesNacional": true, "isMei": false, "isMatriz": true, "city": "Barueri", "state": "SP", "neighborhood": "Alphaville Centro Industrial E Empresarial/Alphav", "street": "Rio Negro, 503", "zipCode": "06454000", "matchedQuery": "tecnologia", "scrapedAt": "2026-10-24T18:07:59.445160+00:00" } Apify datasets export as JSON, CSV, XML, Excel, HTML table, RSS or JSONL.
Understanding your results
The fields that matter most:
-
cnpj/cnpjFormatted: the 14 digits, and the same number asXX.XXX.XXX/XXXX-XX. -
legalName/tradeName: razão social and nome fantasia. In the five sample rows, 3 had no trade name (null). -
status:ATIVAorBAIXADA; withstatus: "ALL"other registry statuses such asINAPTAorSUSPENSAcan appear. -
cnaeCode/cnaeDescription: the primary activity. The five sample rows had five different codes. -
shareCapital: registered capital in reais, from 1,000 to 150,000 across the sample. -
companySize:MEI,MICRO EMPRESA,EMPRESA DE PEQUENO PORTEorDEMAIS(other).DEMAISis also what the Actor writes when the registry lookup gives no size. -
isSimplesNacional,isMei: tax-regime flags.isMatriz: true for a headquarters, false for a branch. -
openingDate: the registration start date asYYYY-MM-DD. All five sample rows opened in 2011. -
city,neighborhood,street,zipCode: the registered address;streetjoins the street name and number, and was null in one sample row. -
matchedQuery: which keyword produced the row.
Pro tips
- Search names, filter activities. The keyword only looks at company names. One sample row had "TECNOLOGIA" in its name and an equipment-installation CNAE, so add
cnaeCodewhen the activity matters. - Use short, distinctive keywords. Every word must appear in a name, so
tecnologia sistemaskeeps fewer companies thantecnologia. - Batch keywords in one run. Put several in
searchQueries; a company saved under one keyword is not saved again under the next. - Bound by capital, not just count.
minCapitalandmaxCapitalare a quick proxy for company size when building lead lists. - Start small. Keep
maxItems: 5until your downstream code handles the fields, then raise it. - Automate the hand-off. A webhook on
ACTOR.RUN.SUCCEEDEDhands your code the finished run, so a scheduled search can land in a CRM or a sheet without polling. Key your table oncnpj: it identifies the company, so a rerun can update rows instead of duplicating them.
Troubleshooting
- Zero rows: a run that is blocked or cannot read a search page ends FAILED. A search that loads normally but returns nothing your filters keep ends SUCCEEDED with an empty dataset and a status message saying so. Loosen the keyword, the state or the capital band.
- Fewer rows than expected: read the summary line near the end of the log,
Run finished. Total companies pushed: N. Rejection breakdown: {...}. It counts drops such askeyword_mismatch,state_mismatchandcapital_out_of_range. From Python:client.log(run.id).get(). - Fields are null: when the registry lookup for a CNPJ fails, the row keeps the search fields only (
companySizefalls back toDEMAIS); with a specificstate, such rows are dropped. - Input refused: a
stateorstatusvalue outside the list.
For help, use the Actor's Store page.
Pricing and next steps
For current pricing and details, see the Store page.
To repeat a search, save the input as a task and add an Apify schedule, which runs it on a cron expression (0 8 * * * is every day at 8 AM). To react when a run finishes, add a webhook on ACTOR.RUN.SUCCEEDED; its payload carries the run object, including defaultDatasetId.
Which sectors or states are you planning to map? Drop a comment! 💬


