API da ANVISA: o que a documentação não conta

Vitor Ayres (abre em nova aba)

Desenho de plantas gerado a partir do primeiro commit do repositório anvisa-api.
Plantas geradas pelo primeiro commit de anvisa-api (a105c6b).

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-Agent padrã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. Com Accept: */* 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 /udi exige pelo menos uma chave em filter.
  • POST /fila/consulta e POST /lista/consulta exigem filter.subfila. Isso mesmo: a de lista também pede subfila.

Chaves de filtro que verificamos:

  • udi: nomeComercial (trecho do nome), udiDi (valor exato), cnpjDetentora, codigoGmdn e nuRegistro.
  • nomeTecnico: nomeTecnico (trecho do nome) e categoriaProduto.
  • 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 foram GET /udi/{id}/download e o arquivo semanal de GET /udi/historico/{id}/download. Com Accept: */* o arquivo vem.
  • Quando a resposta informa o Content-Type, ele é application/vnd.ms-excel, mesmo que o arquivo seja um .xlsx ou um zip.
  • POST /assunto/downloadAssuntoFormulario recebe como corpo JSON só um número inteiro, em vez do objeto que o documento declara. A resposta vem sem Content-Type e sem Content-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.

Como podemos ajudar?

Oi (abre em nova aba)