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:

  1. Python 3.11+ (apify-client 3.3.0 requires it).
  2. The Apify API client for Python.
  3. 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, ...) or ALL for nationwide. Default SP.
  • status: ATIVA (active), BAIXADA (closed) or ALL. Default ATIVA.

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.

⚠️ state and status only accept the listed values; anything else is refused before the run starts. Maranhão (MA) is not in the state list: use ALL and filter state in 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 maxItems or 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 as XX.XXX.XXX/XXXX-XX.
  • legalName / tradeName: razão social and nome fantasia. In the five sample rows, 3 had no trade name (null).
  • status: ATIVA or BAIXADA; with status: "ALL" other registry statuses such as INAPTA or SUSPENSA can 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 PORTE or DEMAIS (other). DEMAIS is 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 as YYYY-MM-DD. All five sample rows opened in 2011.
  • city, neighborhood, street, zipCode: the registered address; street joins the street name and number, and was null in one sample row.
  • matchedQuery: which keyword produced the row.

Pro tips

  1. 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 cnaeCode when the activity matters.
  2. Use short, distinctive keywords. Every word must appear in a name, so tecnologia sistemas keeps fewer companies than tecnologia.
  3. Batch keywords in one run. Put several in searchQueries; a company saved under one keyword is not saved again under the next.
  4. Bound by capital, not just count. minCapital and maxCapital are a quick proxy for company size when building lead lists.
  5. Start small. Keep maxItems: 5 until your downstream code handles the fields, then raise it.
  6. Automate the hand-off. A webhook on ACTOR.RUN.SUCCEEDED hands your code the finished run, so a scheduled search can land in a CRM or a sheet without polling. Key your table on cnpj: 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 as keyword_mismatch, state_mismatch and capital_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 (companySize falls back to DEMAIS); with a specific state, such rows are dropped.
  • Input refused: a state or status value 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! 💬