API da ANVISA: o que a documentação não conta
Vitor Ayres (abre em nova aba)
A ANVISA publica uma API oficial, a Consultas Externas, com fila de análise, UDI de dispositivos médicos, termos GMDN, nomes técnicos, listas e assuntos de peticionamento. Ela vem com um documento OpenAPI. Na prática, esse documento erra ou omite boa parte do que importa para a chamada funcionar.
Este texto registra o que a API faz de verdade. Os comportamentos descritos foram verificados em chamadas reais, e a API pode mudar depois disso.
Resumo
- O
User-Agentpadrão do curl recebe 403. - O token dura 1740 segundos, e o tutorial do portal diz 300. Não há refresh token.
- O limite é uma rajada de 25 requisições, com reposição de 1 por segundo, contado por endereço de origem.
- Erros de validação voltam como HTTP 500.
- A paginação começa em 1 no pedido e em 0 na resposta.
- Quase todo download responde 500 a
Accept: application/json. ComAccept: */*o arquivo vem. - O portal documenta 35 rotas a mais. As sete que testamos responderam 404.
O comportamento real, ponto a ponto
| Tema | O que acontece |
|---|---|
| User-Agent | O Cloudflare responde 403 ao User-Agent padrão do curl |
| Autenticação | OAuth 2.0 client credentials, token de 1740 s, sem refresh token |
| Limite de requisições | Rajada de 25, reposição de 1 por segundo, por endereço de origem |
| Paginação | Pedido começa em 1, resposta começa em 0 |
| Filtros | Algumas consultas exigem um filtro mesmo quando você quer tudo |
| Erros | Falha de validação volta como HTTP 500, com a mensagem no corpo |
| Fila e lista | Devolvem a subfila inteira e ignoram page e size |
| Datas | Inteiros, em milissegundos desde 1970 |
| Downloads | Erro 500 com Accept: application/json em quase todos |
User-Agent: o primeiro 403
A API fica atrás do Cloudflare. Ele responde 403 ao User-Agent padrão do curl, até para a URL do próprio documento OpenAPI. A saída é enviar um User-Agent que descreva o seu programa.
É o primeiro obstáculo e o mais confuso, porque parece erro de credencial e não é.
Autenticação
A autenticação é OAuth 2.0 no fluxo client credentials. O servidor é um Keycloak, no realm externo. As credenciais saem do Portal de APIs da ANVISA, com login Gov.br: um Client ID e um Client Secret.
Três detalhes que encontramos:
- O token dura 1740 segundos (29 minutos). O tutorial do portal diz 300.
- Não há refresh token. Quando o token vence, o programa pede outro.
- O token traz um único papel,
CONSULTASEXTERNAS_LEITURA.
Vale guardar o token e renovar um pouco antes do vencimento, em vez de pedir um novo a cada chamada.
Limite de requisições
O gateway usa um token bucket: aceita uma rajada de 25 requisições e repõe 1 por segundo. O limite aparece só nos cabeçalhos X-RateLimit-* das respostas.
O saldo é contado por endereço de origem, não por Client ID. As rotas públicas do portal, que nem pedem autenticação, passam pelo mesmo gateway e gastam o mesmo saldo. Programas que saem pelo mesmo endereço dividem o limite, mesmo com credenciais diferentes.
Paginação
No pedido, as páginas começam em 1. Enviar page: 0 devolve erro. As respostas são objetos Page do Spring, que começam em 0. A página que você pede como 1 volta identificada como 0.
Filtros obrigatórios
POST /udiexige pelo menos uma chave emfilter.POST /fila/consultaePOST /lista/consultaexigemfilter.subfila. Isso mesmo: a de lista também pedesubfila.
Chaves de filtro que verificamos:
udi:nomeComercial(trecho do nome),udiDi(valor exato),cnpjDetentora,codigoGmdnenuRegistro.nomeTecnico:nomeTecnico(trecho do nome) ecategoriaProduto.termoGmdn:conteudo.
POST /assunto/ estava quebrado nos nossos testes: respondia 500 e não lia o corpo enviado.
Erros
Falhas de validação voltam como HTTP 500. O corpo traz os campos status, mensagem, data_hora e mensagem_detalhada.
Isso muda o tratamento de erro. Um 500 aqui muitas vezes quer dizer “o pedido está errado”, e tentar de novo não resolve. É preciso ler a mensagem.
Fila e lista
fila/consulta e lista/consulta devolvem a subfila ou sublista inteira em um array. Vimos respostas com 40, 35, 71 e 555 linhas. Os parâmetros page e size são ignorados.
Uma subfila vazia responde 404 com corpo vazio. Um código de subfila que não existe recebe a mesma resposta, então não dá para distinguir os dois casos.
Datas
As datas vêm como inteiros, em milissegundos desde 1º de janeiro de 1970.
Downloads
Aqui o documento OpenAPI descreve uma resposta 200 OK sem conteúdo e não fala do cabeçalho Accept. Na prática, é outra história.
- Com
Accept: application/json, quase todo download responde 500, com a mensagem “Could not find acceptable representation”. As exceções que vimos foramGET /udi/{id}/downloade o arquivo semanal deGET /udi/historico/{id}/download. ComAccept: */*o arquivo vem. - Quando a resposta informa o
Content-Type, ele éapplication/vnd.ms-excel, mesmo que o arquivo seja um.xlsxou um zip. POST /assunto/downloadAssuntoFormulariorecebe como corpo JSON só um número inteiro, em vez do objeto que o documento declara. A resposta vem semContent-Typee semContent-Disposition.- No download da fila (
POST /fila/downloadfila), uma subfila vazia responde 500. Na consulta, uma subfila vazia responde 404.
O que está documentado e ainda não existe
O documento OpenAPI tem 32 rotas. As páginas do portal descrevem mais 35, no mesmo endereço base: certificados, empresa nacional e internacional, dossiê, alimentos e produtos de saúde.
Testamos sete delas. Todas responderam o 404 padrão do Spring. Pelo que vimos, estão documentadas e ainda não estão no ar.
Os mesmos conjuntos de dados existem em CSV no portal de dados abertos da ANVISA. O arquivo que conferimos, o de produtos irregulares, era atualizado nos dias úteis.
Como verificamos
Fizemos chamadas reais à API e gravamos as respostas. Elas estão no repositório anvisa-api, na pasta fixtures, com um manifesto. Os testes do cliente rodam sobre essas respostas gravadas.
As correções ao documento da ANVISA estão em um arquivo à parte, no formato OpenAPI Overlay. O documento original fica como foi publicado, só reformatado, e dá para ver exatamente o que mudamos.
Um processo automático baixa de novo, duas vezes por mês, as páginas do portal sobre as rotas que ainda não estão no ar. Se a ANVISA mudar essas páginas, isso aparece como um diff no repositório.
As filas de análise também saem todo dia como feeds Atom, no projeto anvisa-feeds.
O que não sabemos: se esses comportamentos são intencionais e por quanto tempo vão continuar assim.
Um cliente que já trata isso
Publicamos um cliente em Python com licença MIT. Ele envia o User-Agent, guarda o token e renova antes do vencimento, respeita o limite do gateway para não receber 429 (requisições demais), usa Accept: */* nos downloads e transforma a maior parte dos erros 500 em exceções específicas.
uv tool install anvisa # ou: pipx install anvisa
As credenciais vêm das variáveis ANVISA_CLIENT_ID e ANVISA_CLIENT_SECRET. O número 167 no exemplo é o código de uma subfila.
from anvisa import Client
with Client.from_env() as anvisa:
for row in anvisa.fila.consulta(167):
print(row.nuOrdem, row.numeroProcessoFormatado, row.dsAssunto)
for device in anvisa.udi.iter_search(nomeComercial="cateter"):
print(device.udiDi, device.nomeComercial)
Ele cobre as 32 rotas do documento publicado. O projeto não tem vínculo com a ANVISA.
Quem integra com serviços da Receita e da SEFAZ encontra o mesmo tipo de surpresa; o post sobre baixar NFS-e, NF-e e CT-e trata disso.
Se a sua empresa depende de um sistema ou de uma API que não se comporta como a documentação diz, é esse o trabalho que fazemos em engenharia reversa e em integrações.
Perguntas
- Como obter credenciais para a API da ANVISA?
- No Portal de APIs da ANVISA, com login Gov.br. O portal gera um Client ID e um Client Secret, usados para pedir o token.
- Quanto tempo dura o token da API da ANVISA?
- 1740 segundos, ou 29 minutos, na nossa verificação. O tutorial do portal fala em 300 segundos. Não há refresh token: quando o token vence, é preciso pedir outro.
- Qual é o limite de requisições da API da ANVISA?
- O gateway aceita uma rajada de 25 requisições e repõe uma por segundo. O saldo aparece só nos cabeçalhos X-RateLimit e é contado por endereço de origem, não por credencial.
- Por que o download da API da ANVISA responde erro 500?
- Quase sempre porque a requisição pede JSON. Com o cabeçalho Accept: application/json, quase todo download responde 500; com Accept: */*, o arquivo vem. No download da fila, uma subfila vazia também responde 500.
- Dá para consultar certificados, empresas e alimentos pela API?
- Ainda não, pelo que testamos. O portal documenta essas rotas, mas as sete que testamos, entre elas a de empresa, responderam 404. Os mesmos conjuntos de dados existem em CSV no portal de dados abertos da ANVISA.