# Brazil ANVISA Registered Products Lookup (`jgoia/anvisa-products-lookup`) Actor

Lookup/search Brazil's official ANVISA registry of registered products - medications, cosmetics, sanitizers, and health products/medical devices - by registration number, product name, or manufacturer CNPJ. Charged per matched product record returned.

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

## Pricing

$20.00 / 1,000 matched product record returneds

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/actors/running/actors-in-store.md#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 ANVISA Registered Products Lookup (Medicamentos, Cosméticos, Saneantes, Produtos para Saúde)

Look up or search Brazil's official ANVISA (health regulatory agency)
registry of registered products — medications, cosmetics, sanitizers, and
health products/medical devices — by registration number, product name,
or manufacturer/registration-holder CNPJ. Get back clean, normalized
JSON: product name, registration number, status, expiry, manufacturer,
and (for medications) active ingredients and therapeutic class, or (for
medical devices) risk class and physical manufacturer.

### Who this is for

Pharmacies, distributors, importers, and marketplaces that need to
confirm a product is genuinely ANVISA-registered before selling/listing
it; regulatory-affairs and compliance teams tracking registration status
and expiry dates across a portfolio; procurement/vendor-vetting workflows
checking whether a supplier's products are validly registered; or anyone
building on top of ANVISA's registries without wanting to hand-parse its
raw open-data CSVs directly.

### What you send in

```json
{
  "registrationNumbers": ["125680040"],
  "productNames": ["dipirona sodica"],
  "manufacturerCnpjs": ["73856593000166"],
  "categories": ["medicamento"],
  "maxResultsPerQuery": 50
}
```

- `registrationNumbers` — exact-match lookup by ANVISA registration
  number, punctuation optional. Searched across every selected category
  (a bare number doesn't indicate which registry it belongs to).
- `productNames` — accent-/case-insensitive substring search, e.g.
  `"dipirona"` matches `"DIPIRONA SODICA"`. Capped by `maxResultsPerQuery`.
- `manufacturerCnpjs` — exact-match search by the CNPJ of the
  registration holder (*detentor do registro*), punctuation optional.
  Capped by `maxResultsPerQuery`.
- `categories` — which of the four registries to search:
  `medicamento`, `cosmetico`, `saneante`, `produto_saude`. Defaults to
  all four if omitted.
- `maxResultsPerQuery` — default 50, max 200. Only applies to
  `productNames`/`manufacturerCnpjs` searches (summed across all
  categories searched); registration-number lookups are always
  exact-match and uncapped.

At least one of `registrationNumbers`, `productNames`, or
`manufacturerCnpjs` is required.

### What you get back

One dataset record per matched product:

```json
{
  "category": "medicamento",
  "product_name": "DIPIRONA SODICA",
  "registration_number": "125680040",
  "process_number": "253510395140179",
  "status": "Ativo",
  "expiry_date": "2027-01",
  "expiry_date_raw": "012027",
  "manufacturer": { "cnpj": "73856593000166", "name": "PRATI DONADUZZI & CIA LTDA" },
  "regulatory_category": "Genérico",
  "therapeutic_class": "ANALGESICOS NAO NARCOTICOS",
  "active_ingredients": ["dipirona"],
  "risk_class": null,
  "device_manufacturer": null,
  "_meta": { "source_category": "medicamento", "source_file": "DADOS_ABERTOS_MEDICAMENTOS.csv", "retrieved_at": "2026-08-17T23:06:31Z" }
}
```

`risk_class` (Classe de Risco I–IV) and `device_manufacturer` (the
physical maker, which can differ from the registration holder — e.g. an
importer holding the registration for a foreign-made device) are only
populated for `produto_saude`. `therapeutic_class`/`active_ingredients`/
`regulatory_category` are only meaningfully populated for `medicamento`.

A query that finds no match still gets a visibility record (not charged),
so you can see what happened to every input:

```json
{ "query_type": "registration_number", "query_input": "000000000", "registration_number": "000000000", "status": "not_found", "categories_searched": ["medicamento"] }
{ "query_type": "product_name", "query_input": "not a real drug xyz", "status": "no_matches", "categories_searched": ["medicamento", "cosmetico", "saneante", "produto_saude"] }
```

**Note on `status`**: passed through exactly as ANVISA publishes it, not
collapsed into a single "active/inactive" boolean — the four registries
use genuinely different, not-fully-verified vocabularies (see "What was
actually tested vs. not" below for why).

### Pricing

Pay-per-event: **you are charged only for matched product records
actually returned** (event `product-record`) — the same "charge only for
delivered value" rule as `cnpj-lookup`. A query that legitimately matches
nothing is not charged. See the Actor's Store page for the current
per-event price.

**Pricing rationale**: priced at the same tier as `pncp-lookup`
($0.02/record), not `cnpj-lookup`'s $0.01. The underlying data is free
open data either way, but this Actor's real operational cost per run is
higher than `cnpj-lookup`'s: it downloads and parses whole bulk CSV files
(2.3KB–27MB, ~39MB combined across all four categories) rather than
making one lightweight per-record JSON API call, and the deliverable —
a point-in-time registry fact (is this product genuinely registered,
what's its status/expiry) — sits in the same value tier as `cnpj-lookup`'s
company-registry lookup rather than the higher compliance-grade tier
`ceis-lookup` charges ($0.03) for a completed sanctions check regardless
of outcome.

### Data source, legal basis & terms of use

Source: ANVISA's official open-data portal at `dados.anvisa.gov.br`,
published under Brazil's federal Open Data Policy (**Decreto nº
8.777/2016**) as part of ANVISA's own "Plano de Dados Abertos" (PDA) —
verified directly (not assumed): ANVISA's PDA overview page
(`gov.br/anvisa/pt-br/acessoainformacao/dadosabertos`) states it publishes
data "em compliance com... Decreto nº 8.777, de 11 de maio de 2016", and
Brazil's federal open-data definition (`gov.br/governodigital`) states
open licensing "dá a autorização para que esses dados sejam utilizados
por qualquer pessoa, sem restrições, e para qualquer fim, **inclusive
comerciais**" (authorizes use by anyone, without restriction, for any
purpose, **including commercial**) — the same statutory basis already
established and cleared for `pncp-lookup` (see `DECISIONS.md` ADR-010),
with only an attribution/share-alike condition, no field found narrowing
or contradicting it for these specific datasets.

**No API key, no registration, no Gov.br login** — unlike CEIS/CEPIM/CNEP
(`ceis-lookup`), which required a Gov.br-authenticated key from Portal da
Transparência. `dados.anvisa.gov.br` serves plain, unauthenticated CSV
file downloads. `api.anvisa.gov.br` (ANVISA's separate, newer REST API
portal, "Portal APIs ANVISA") **is** Client-ID/Client-Secret-gated behind
Gov.br login (confirmed: it sits behind Cloudflare bot-protection that
returned HTTP 403 to a direct fetch, and independent reporting confirms
credentials require Gov.br authentication) — this Actor deliberately
avoids that gated API and uses the open-data CSVs instead, which cover
the same underlying registries with no such gate.

Four separate CSV files back the four categories (see
`anvisa_products_lookup/sources.py` for the full reasoning behind each
pick, including two cases where the obviously-named file turned out to
be wrong):

| Category | File | Rows (2026-08-17) | Backed by a published data dictionary? |
|---|---|---|---|
| Medicamentos | `DADOS_ABERTOS_MEDICAMENTOS.csv` | 43,455 | Yes |
| Cosméticos | `DADOS_ABERTOS_COSMETICO.csv` | 13 | Yes |
| Saneantes | `TA_DA_PRODUTO_SANEANTE.CSV` | 28,626 | Yes |
| Produtos para Saúde | `TA_PRODUTO_SAUDE_SITE.csv` | 115,111 | No (see below) |

**Cosméticos scope note**: 13 rows is not a bug — Brazil's cosmetics
regulation only requires individual ANVISA *registration* for "Grau 2"
(higher-risk, e.g. sunscreens, hair dyes) products; the much larger
population of "Grau 1" cosmetics is only *notified* (self-declared, no
individual registration), so it isn't in this open dataset at all.

**Produtos para Saúde lower-certainty note**: `TA_PRODUTO_SAUDE_SITE.csv`
has no matching ANVISA-published data dictionary PDF (every other file
does). It's used anyway because: it lives in the same open,
unauthenticated `dados.anvisa.gov.br` domain as the documented files; its
field names are self-explanatory and its content was spot-checked against
what `consultas.anvisa.gov.br`'s public search UI shows for the same
registration numbers; and its `DT_ATUALIZACAO_DADO` column shows it
refreshed within 24 hours of verification — clearly live and
actively maintained. Disclosed as a lower-certainty source than the other
three, not silently treated as equally authoritative.

**Two real quirks discovered and handled, not glossed over**:

1. **Inconsistent date formats across files** — ANVISA's own CSVs are not
   internally consistent: some date fields are `dd/mm/yyyy`, others are
   `mm/dd/yyyy`, determined empirically per field using unambiguous
   (day>12) examples, not assumed. Full writeup with verified examples:
   `anvisa_products_lookup/dates.py` module docstring.
2. **Server-side TLS chain misconfiguration** — `dados.anvisa.gov.br`
   does not send its intermediate CA certificate, so a fresh HTTP client
   (this Actor, on Apify's infra) fails `CERTIFICATE_VERIFY_FAILED` by
   default. Verified as a genuine server misconfig (not a stale local CA
   store) by reproducing the failure with both the system CA bundle and
   an independently-maintained `certifi` bundle, then fixing it by
   fetching the exact missing intermediate from Sectigo's own certificate
   repository and adding it to the trust store — full writeup and SHA-256
   fingerprint for independent verification: `anvisa_products_lookup/certs.py`.

### Rate limits & fair use

None published, because this isn't a rate-limited query API — it's a
bulk file download, refetched once per Actor run regardless of how many
queries you send in that run.

***

### For the Aurora project (internal)

Fourth product, built same-day as \[`bcb-institutions-lookup`],
\[`tse-electoral-lookup`], and \[`ceis-lookup`] per `DECISIONS.md` ADR-008's
owner-directed expansion (this specific product completes the earlier
`OPPORTUNITIES.md`/ADR-004 "government-portal/restrictive-API niche data
reseller" thread with a fourth Brazilian official-data Actor). Picked up
from a prior session that was interrupted by a Claude usage session limit
right after research but before any code was written — this build redid
the access/terms verification directly rather than trusting the prior
session's unwritten notes.

#### Feasibility verdict: feasible, built, no ToS blocker found

Unlike DataJud (`datajud-lookup`, ADR-009 — killed by an explicit
anti-commercial clause) and unlike CEIS (`ceis-lookup` — cleared, but
gated behind a Gov.br-login-only API key), ANVISA's open-data CSVs are
**both** legally clear (Decreto 8.777/2016 statutory basis, same as
`pncp-lookup`) **and** technically open (no key, no login, no gate) —
the best combination found across all four products in this batch. The
one real technical blocker found (the TLS chain misconfiguration above)
was a genuine infrastructure quirk, not a business/legal one, and had a
clean fix rather than a walk-away.

#### What was actually tested vs. not

**Tested and passing**: 76/76 tests (`pytest`), `ruff` clean. Covers:

- `dates.py` — all three date-format parsers, each against the exact
  real unambiguous example value that determined that field's
  convention (not synthetic dates).
- `text_utils.py` — including both real data-quality quirks found in the
  source (HTML-entity-encoded characters, a mojibake `"?"` artifact),
  CNPJ/name splitting (including a razão social containing its own
  `" - "`, which a naive first-hyphen split would have broken), and
  accent-/case-insensitive matching.
- `normalize.py` — all four category normalizers, tested against **real
  rows captured directly from the live CSVs** on 2026-08-17 (not
  hand-written fixtures) — `tests/fixtures/*.csv`, byte-preserved
  (cp1252) rather than re-encoded through a CSV writer. Covers the
  `produto_saude` multi-manufacturer-per-registration case and the
  `"VIGENTE"` non-date sentinel specifically.
- `sources.py` — category resolution/validation.
- `client.py` — download retry/backoff, non-retried 4xx, and (mirroring
  the exact bug class `pncp-lookup` shipped and had to fix post-build,
  ADR-010) that a bare `httpx` timeout/connect error is always wrapped,
  never left to crash the Actor — via `httpx.MockTransport`, no real
  network calls in the automated suite.
- `certs.py` — the embedded certificate loads into a real `ssl.SSLContext`
  without error and verification stays `CERT_REQUIRED` (the fix adds a
  trusted intermediate, it never disables verification).
- `search.py` — exact/substring/accent-insensitive matching and the
  `maxResultsPerQuery` cap.
- `main.py` — the pure `_coerce_str_list` input-parsing helper. Like
  `ceis-lookup`'s test suite, the full `async with Actor:` lifecycle
  isn't exercised by the automated unit tests (no `ACTOR_*` env vars in
  this test environment) — covered instead by the live `apify-cli run`
  below.

**Live end-to-end verification against the real internet** (this
developer does not hold, and this product needs, no credential at all —
unlike `ceis-lookup`'s disclosed gap):

- Downloaded and parsed all four real CSVs directly (43,455 / 13 /
  28,626 / 115,111 rows respectively) and ran real registration-number,
  product-name, and manufacturer-CNPJ searches against each — including
  the DIPIRONA SODICA record used as the running example above
  (registration `125680040`, Prati Donaduzzi, a real, well-known,
  currently-active Brazilian medication) and real matches in all three
  other categories (a sunscreen in cosméticos, a household cleaner in
  saneantes, a condom brand in produtos para saúde).
- Ran the actual Actor entry point via the official `npx apify-cli run`
  (which reads `.actor/actor.json` exactly as the real platform would),
  not just the library functions directly — confirmed correct dedup
  (a registration-number query and an overlapping product-name query in
  the same run charge the matched record only once), correct "not found"
  handling for a registration number verified not to exist, and a clean
  exit code 0. `apify-cli validate-schema` passes for both schemas.
- Independently diagnosed and fixed the TLS chain issue live (see above)
  — `curl`, `openssl s_client`, and Python's `ssl` module against both
  the system CA store and `certifi`'s bundle all reproduced the same
  failure before the fix, and all three stopped failing after it.

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

- Real pay-per-event **charging** behavior: running locally/via
  `apify-cli run`, the Apify SDK correctly no-ops
  `Actor.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 exists outside
  a real platform run — same disclosed limitation as `cnpj-lookup`/
  `pncp-lookup`. The charging *code path* (including the
  `event_charge_limit_reached` handling) is exercised on every match but
  only confirmable after deploying and monetizing on the real platform.
- **Docker build**: no Docker on this VPS (no root/sudo, same standing
  constraint as every prior product — see `AGENTS.md`). The Dockerfile
  is byte-for-byte the same template already proven to build successfully
  on Apify's infrastructure three times (`cnpj-lookup` ADR-006,
  `pncp-lookup` ADR-011), with only the package name changed.
- Whether Apify's platform container has the same missing-intermediate
  TLS problem independently of this VPS — moot either way, since the fix
  (bundling the intermediate) doesn't depend on which CA store the
  container starts with, but the *first live platform run* is still the
  first time this exact code path runs somewhere other than this VPS —
  flagged as the first thing to double-check during deployment, the same
  "don't assume, verify on the real platform" discipline `pncp-lookup`
  applied to its own VPS-vs-platform networking gap (ADR-011).
- No real paid usage, no Apify Store listing yet.

#### Deployment (status: built, tested, zero owner-only blockers found)

Unlike `ceis-lookup` (blocked on a Gov.br-login API key) and unlike the
very first product `cnpj-lookup` (which needed one-time payout/profile
setup before its *first-ever* publish), this product needs **no new
owner action at all** — payout billing info, public profile, and Store
Terms acceptance are Apify account-level settings already completed
during `cnpj-lookup`'s deployment (ADR-007) and confirmed to carry over
automatically to every subsequent Actor on the same account
(`pncp-lookup`, ADR-011). The remaining steps are the same proven
sequence, mechanical rather than blocked:

1. `cd products/anvisa-products-lookup && npx apify-cli push` — builds
   and deploys on Apify's infrastructure.
2. Real remote smoke test: `npx apify-cli call -i '{"registrationNumbers": ["125680040"], "categories": ["medicamento"]}' -o`
   — should return the DIPIRONA SODICA record shown above. Worth
   specifically confirming the TLS fix works unchanged on Apify's actual
   container (see "Not tested" above).
3. Set pay-per-event pricing (`.actor/pay_per_event.json`'s
   `product-record` event at $0.02) and publish (`isPublic: true`,
   categories Business + Lead generation, matching the prior three) via
   the Apify REST API `PUT /v2/acts/{actorId}` — the same calls already
   proven working on this account three times (`cnpj-lookup` ADR-007,
   `pncp-lookup` ADR-011), should need no further owner action.
4. Optional, non-blocking: a Store logo (`assets/` directory reserved,
   empty) — same "ready but not required to publish" status `pncp-lookup`
   shipped with (ADR-011).
5. Report real usage/revenue into `BUSINESS.md` once it exists — do not
   fabricate numbers before they exist.

# Actor input Schema

## `registrationNumbers` (type: `array`):

ANVISA registration/cadastro numbers to look up (exact match), punctuation optional. Searched across all selected categories, since the number alone doesn't indicate which one it belongs to.

## `productNames` (type: `array`):

Product name(s) to search for (accent-/case-insensitive substring match), e.g. "dipirona". Results are capped by maxResultsPerQuery.

## `manufacturerCnpjs` (type: `array`):

CNPJ(s) of the registration holder (detentor do registro) to search for (exact match), punctuation optional. Results are capped by maxResultsPerQuery.

## `categories` (type: `array`):

Which ANVISA registries to search. Defaults to all four if left empty.

## `maxResultsPerQuery` (type: `integer`):

Caps how many matched products are returned for each product-name or manufacturer-CNPJ search term (summed across all searched categories). Registration-number lookups are always exact-match and are not capped by this. Max 200.

## Actor input object example

```json
{
  "registrationNumbers": [
    "125680040"
  ],
  "productNames": [
    "dipirona sodica"
  ],
  "manufacturerCnpjs": [],
  "categories": [],
  "maxResultsPerQuery": 50
}
```

# 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 = {
    "registrationNumbers": [
        "125680040"
    ],
    "productNames": [
        "dipirona sodica"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jgoia/anvisa-products-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 = {
    "registrationNumbers": ["125680040"],
    "productNames": ["dipirona sodica"],
}

# Run the Actor and wait for it to finish
run = client.actor("jgoia/anvisa-products-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 '{
  "registrationNumbers": [
    "125680040"
  ],
  "productNames": [
    "dipirona sodica"
  ]
}' |
apify call jgoia/anvisa-products-lookup --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jgoia/anvisa-products-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/EAWWrHQtGojBv2tl0/builds/D0BIVwsRI9A8U60NS/openapi.json
