Pular para o conteúdo
VigiaXML
Desenvolvedores

Documentação da API VigiaXML

REST, JSON, autenticação via header X-Api-Key. Consulta NF-e, CT-e, MDF-e direto na SEFAZ. Monitoramento de 30 dias por chave. Importação de lotes mistos. Exemplos cURL e TypeScript prontos pra colar.

Base URL: https://api.vigiaxml.com · Trial gratuito: 30 dias ou 5.000 consultas — o que vier primeiro.

Quickstart

Da chave à primeira consulta em 5 minutos

Cadastre-se em /cadastro (1 min) → confirme o email → copie a api-key (mostrada uma única vez) → consulte. Trial gratuito não exige cartão.

Prefere OpenAPI? A especificação está em api.vigiaxml.com/v1/swagger/v1.json (importe no Postman ou gere um client) e a UI interativa em api.vigiaxml.com/v1/docs. Cobre só as rotas por api-key, com schema de resposta nos endpoints de sincronização.

quickstart.shbash
# 1. Exporte a api-key copiada após confirmar o email
export VIGIA_API_KEY="vxml_..."

# 2. Consulte uma chave (a API detecta o modelo pelas posições 21-22)
curl -X POST https://api.vigiaxml.com/v1/consulta \
  -H "X-Api-Key: $VIGIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "chaveAcesso": "35240300000000000000550010000001231123456785" }'

# 3. Coloque a mesma chave em monitoramento contínuo (30 dias)
curl -X POST https://api.vigiaxml.com/v1/monitoramento \
  -H "X-Api-Key: $VIGIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chaveAcesso": "35240300000000000000550010000001231123456785",
    "callbackUrl": "https://erp.example.com/webhooks/vigia"
  }'
Cadastro

Sign-up self-service e api-key

POST/v1/auth/signupPOST/v1/auth/regenerate-api-key

O cadastro cria um tenant (sua carteira) + um user (o login do master) num só passo. Confirmação por email é obrigatória — sem ela, a api-key não é gerada. A chave plana aparece uma única vez na tela de confirmação. Se perder, gere outra via POST /v1/auth/regenerate-api-key (cookie de sessão) — a anterior é revogada.

cadastro.shbash
# 1. Cadastro self-service (não requer api-key)
curl -X POST https://api.vigiaxml.com/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "nomeUsuario": "Maria Souza",
    "nomeEmpresa": "Acme Factoring LTDA",
    "email": "maria@acme.com.br",
    "cnpj": "12345678000190",
    "senha": "uma-senha-forte-de-12-chars",
    "telefone": "11999990000",
    "aceiteTos": true
  }'

# Resposta 202 — independente de email já existir (anti-enumeração).
# Email de confirmação chega em segundos com link /verifique-email?token=...

# 2. Após confirmar pelo link, a tela mostra a api-key UMA ÚNICA VEZ.
# Você também pode girar a chave depois:
curl -X POST https://api.vigiaxml.com/v1/auth/regenerate-api-key \
  -H "Cookie: vxml_session=..." \
  -H "Content-Type: application/json"
Anti-enumeração: o endpoint sempre devolve 202 com a mesma mensagem, mesmo se o email/CNPJ já existir. Atacantes não conseguem mapear sua base de clientes.
Autenticação

API key por carteira (header X-Api-Key)

Cada api-key pertence a uma carteira (tenant). Chaves são prefixadas com vxml_ e armazenadas só como hash bcrypt — não temos como recuperar a chave plana depois.

Envie no header X-Api-Key: vxml_<sua-chave>. A API responde 401 se a chave for inválida ou tiver sido girada.

Existe também auth por cookie de sessão (vxml_session) usado pelo painel /conta — endpoints /v1/auth/login, /v1/auth/me, /v1/auth/logout, /v1/me/consulta, /v1/me/consultas. Para integração server-to-server use api-key.

Boa prática: nunca commit api-keys. Use variáveis de ambiente, secret manager (Azure Key Vault, AWS Secrets Manager) ou um cofre nativo do seu CI/CD.
Endpoint

Consulta avulsa

POST/v1/consultaconsome 1 unidade de consulta avulsa (R$ 0,20)

Faz uma chamada síncrona ao webservice da SEFAZ e devolve o status atual. Auto-detecta o modelo pelas posições 21-22 da chave: NF-e (55), CT-e (57), CT-e OS (67), MDF-e (58). Não cacheado — a resposta reflete o estado oficial naquele instante.

Body

CampoTipoDescrição
chaveAcessostring (obrig.)Chave de acesso de 44 caracteres (numérica, ou com letras A-Z nas posições 7-18 quando o emitente tem CNPJ alfanumérico). Aceita com hífens/espaços (são removidos) e minúscula.
consulta.shbash
curl -X POST https://api.vigiaxml.com/v1/consulta \
  -H "X-Api-Key: $VIGIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chaveAcesso": "35240300000000000000550010000001231123456785"
  }'
consulta.tstypescript
const res = await fetch("https://api.vigiaxml.com/v1/consulta", {
  method: "POST",
  headers: {
    "X-Api-Key": process.env.VIGIA_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    chaveAcesso: "35240300000000000000550010000001231123456785",
  }),
});

if (!res.ok) throw new Error(`Consulta falhou: ${res.status}`);
const data = await res.json();
// data.status: "autorizada" | "cancelada" | "denegada" | "inexistente" | "outro" | "erro"
// data.c_stat: "100" | "101" | "217" | ...   (código oficial SEFAZ)
// data.x_motivo: mensagem oficial SEFAZ
// data.uf: "SP" | "MG" | ...
// data.modelo: "NF-e" | "CT-e" | "CT-e OS" | "MDF-e"
// data.modelo_codigo: 55 | 57 | 58 | 67
// data.duration_ms, data.queried_at, data.consulta_id

Resposta 200

response.jsonjson
{
  "consulta_id": "c5520f10-0511-425d-81b0-ffc75a81ca25",
  "chave_acesso": "35240300000000000000550010000001231123456785",
  "modelo": "NF-e",
  "modelo_codigo": 55,
  "uf": "SP",
  "status": "autorizada",
  "c_stat": "100",
  "x_motivo": "Autorizado o uso da NF-e",
  "situacao": "cStat=100 (consultar Anexo II do MOC NF-e)",
  "duration_ms": 1247,
  "queried_at": "2026-05-03T20:31:11.523Z"
}
Endpoint

Consulta cadastral do contribuinte

GET/v1/cadastro/:cnpj?uf=SPR$ 0,70 por consulta

Consulta a situação cadastral estadual do CNPJ direto na SEFAZ da UF (webservice CadConsultaCadastro4): inscrição estadual habilitada, baixada ou suspensa, razão social, CNAE, regime de apuração e credenciamento como emissor de NF-e/CT-e. É o dado que interessa a quem compra recebível: IE suspensa ou baixada emitindo nota é sinal de atenção — e a resposta reporta o fato oficial, sem classificação jurídica. O parâmetro uf é obrigatório: o cadastro é estadual, não nacional.

Valores de situacao: habilitada, nao_habilitada, baixada, suspensa e nao_cadastrada_na_uf — este último (cStat 259) é resposta válida e cobrada: “o CNPJ não tem inscrição estadual nesta UF” também é informação. A partir de 16/11/2026 (NT 2026.007), empresa que não é contribuinte do ICMS pode emitir NF-e sem inscrição estadual, mas só em operação fora do ICMS (ex.: venda de bem do ativo imobilizado, comodato, remessa para conserto) — NF-e sem IE não documenta venda de mercadoria. Em nota de venda, ou emitida antes dessa data, nao_cadastrada_na_uf continua sinal de atenção. Nos dois casos habilitado vem false. Quando o CNPJ tem várias inscrições na UF (cStat 112), todas vêm em ocorrencias e habilitado é true se qualquer uma estiver ativa.

cadastro-contribuinte.shbash
# Situação cadastral do contribuinte na UF (IE habilitada/baixada/suspensa)
# CNPJ sem máscara na URL — a barra de 11.222.333/0001-81 quebraria a rota.
curl "https://api.vigiaxml.com/v1/cadastro/11222333000181?uf=SP" \
  -H "X-Api-Key: $VIGIA_API_KEY"
response.jsonjson
{
  "success": true,
  "cnpj": "11222333000181",
  "uf": "SP",
  "situacao": "habilitada",
  "habilitado": true,
  "c_stat": "111",
  "x_motivo": "Consulta cadastro com uma ocorrência",
  "ie": "111000111000",
  "razao_social": "ACME COMERCIO LTDA",
  "regime_apuracao": "NORMAL - REGIME PERIODICO DE APURACAO",
  "cnae": "4711302",
  "credenciado_nfe": "1",
  "credenciado_cte": "0",
  "data_inicio_atividade": "2010-01-01",
  "data_ultima_situacao": "2024-06-30",
  "ocorrencias": [
    {
      "ie": "111000111000",
      "c_sit": "1",
      "situacao": "Habilitada",
      "razao_social": "ACME COMERCIO LTDA",
      "endereco": { "municipio": "SAO PAULO", "cep": "01000000" }
    }
  ],
  "fonte": "sefaz",
  "consultado_em": "2026-08-11T18:00:00Z",
  "duration_ms": 1240,
  "valor_estimado_brl": 0.70
}
Cache de 24h: cadastro muda devagar, então repetir o mesmo CNPJ+UF dentro de 24h com a mesma api-key devolve o resultado guardado sem nova cobrança (valor_estimado_brl: 0). Quando o dado fresco veio de outra consulta recente, a resposta vem com "fonte": "cache" — mesmo preço, só que sem esperar a SEFAZ.
Cobertura por UF (medida em ago/2026): com o certificado incluso no serviço, hoje PR e GO respondem a consulta. A maioria das demais SEFAZ (SP, MG, BA, PE, MS e todas as UFs atendidas pela SEFAZ Virtual RS) exige que o certificado consultante seja de um emissor de NF-e credenciado na própria UF — nesses casos a resposta é 422 certificado_sem_credenciamento_na_uf, e a alternativa é usar o seu próprio certificado credenciado (BYOC — fale com o suporte). AM não oferece o serviço (422 cadastro_indisponivel_na_uf). Erro nunca é cobrado, e a cobertura pode mudar conforme as SEFAZ ajustam a política.
Endpoint

Importação em lote

POST/v1/importaraté 1.000 chaves por request

Lote unificado — aceita NF-e + CT-e + MDF-e misturados no mesmo array. Modo avulsa só consulta. Modo monitoramento consulta E adiciona ao monitoramento contínuo de 30 dias. Para lotes grandes, use pularConsultaInline=true — a consulta inicial fica a cargo do worker (até 1h após enfileirar).

importar.shbash
# Lote misto (NF-e + CT-e + MDF-e) — modelo detectado pelas posições 21-22.
curl -X POST https://api.vigiaxml.com/v1/importar \
  -H "X-Api-Key: $VIGIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chaves": [
      "35240300000000000000550010000001231123456785",
      "35240300000000000000570010000001231123456786",
      "35240300000000000000580010000001231123456787"
    ],
    "modo": "monitoramento",
    "webhookUrl": "https://erp.example.com/webhooks/vigia",
    "pularConsultaInline": false
  }'

Resposta 200

response.jsonjson
{
  "modo": "monitoramento",
  "total": 3,
  "duration_ms": 2918,
  "adicionadas": 3,
  "duplicadas": 0,
  "breakdown": { "nfe": 1, "cte": 1, "mdfe": 1 },
  "rejeitadas": [],
  "valor_estimado_brl": 1.05,
  "results": [
    { "chave": "35...", "modelo": 55, "tipo": "NF-e", "uf": "SP",
      "cStat": "100", "status": "autorizada", "xMotivo": "Autorizado o uso da NF-e", "durationMs": 980 }
  ]
}
Validação do emitente no mesmo lote: envie "validarEmitente": true e a API checa a situação cadastral do emitente de cada nota usando o CNPJ e a UF que já estão na própria chave — sem input extra. Emitentes repetidos são deduplicados (50 notas do mesmo emitente = 1 consulta de R$ 0,70) e o cache de 24h vale aqui também. A resposta ganha emitentes (resumo por CNPJ) e cada item de results ganha emitente com situacao/habilitado. O que não couber no orçamento de 12s volta "fora_do_orcamento" — complete depois com GET /v1/cadastro. Emitentes de UF que exige certificado credenciado voltam "requer_credenciamento_na_uf" (sem cobrança — veja a cobertura por UF acima). Não roda junto com pularConsultaInline.
Endpoints

Monitoramento contínuo (30 dias)

POST/v1/monitoramentoconsome 1 unidade de monitoramento (R$ 0,35)
GET/v1/monitoramentoGET/v1/monitoramento/:chaveDELETE/v1/monitoramento/:chave

Coloca a chave em vigilância automática. A cadência é horária no D0 (dia da emissão, onde concentra a maioria dos cancelamentos) e diária do D1 ao D30. A janela de 30 dias começa no momento do POST. DELETE é soft (encerra antecipadamente — o histórico fica preservado).

Os GET daqui não devolvem eventos. Eles respondem “qual a situação desta chave na minha carteira”: status_atual, proxima_consulta_em, expira_em, ativo. Manifestação, passagem, entrega e encerramento vêm de GET /v1/rastreio/:chave (com linha do tempo montada) ou de GET /v1/eventos (lista crua, filtrável por chave e tp).

POST /v1/monitoramento — body

CampoTipoDescrição
chaveAcessostring (obrig.)Chave de acesso de 44 caracteres (aceita CNPJ alfanumérico do emitente). Modelos aceitos: 55, 57, 58, 67.
callbackUrlstring (https)Opcional. Recebe um POST webhook.status_mudou a cada mudança de status da chave — ver Webhooks.
monitoramento.shbash
# Adicionar uma chave ao monitoramento contínuo (vale 30 dias).
curl -X POST https://api.vigiaxml.com/v1/monitoramento \
  -H "X-Api-Key: $VIGIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chaveAcesso": "35240300000000000000550010000001231123456785",
    "callbackUrl": "https://erp.example.com/webhooks/vigia"
  }'

# Listar carteira (paginado)
curl "https://api.vigiaxml.com/v1/monitoramento?ativos=true&limit=50&offset=0" \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Detalhe de uma chave
curl https://api.vigiaxml.com/v1/monitoramento/35240300000000000000550010000001231123456785 \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Encerrar antecipadamente (soft delete — para o polling)
curl -X DELETE https://api.vigiaxml.com/v1/monitoramento/35240300000000000000550010000001231123456785 \
  -H "X-Api-Key: $VIGIA_API_KEY"

Resposta 201 (POST) / 200 (GET detalhe)

response.jsonjson
{
  "id": "5e1d8a64-2c4f-4a3a-9ae8-5a0fbb2c9871",
  "chave": "35240300000000000000550010000001231123456785",
  "modelo": 55,
  "callback_url": "https://erp.example.com/webhooks/vigia",
  "status_atual": "autorizada",
  "criado_em": "2026-05-04T20:33:02Z",
  "expira_em": "2026-06-03T20:33:02Z",
  "proxima_consulta_em": "2026-05-04T21:33:02Z"
}
Dedup automático: tentar adicionar a mesma chave 2x na mesma janela retorna 409 conflict com o monitoramento existente. Use GET /v1/monitoramento/:chave se precisar do estado atual.
Exceção — chave já vigiada por descoberta automática: quando um documento seu referencia outro (ex.: o CT-e de uma NF-e), o VigiaXML passa a vigiar o referenciado sem custo. Se você depois pedir essa chave explicitamente, ela é promovida a monitoramento contratado: resposta 200 com promovida: true, janela de 30 dias a partir do pedido, cadência horária no primeiro dia e cobrança de R$ 0,35. No /v1/importar a contagem vem em promovidas e entra no valor_estimado_brl.
Padrão de integração

Botão “monitorar” com resposta na tela

O caso mais comum dentro de um ERP: o usuário clica em um botão, espera, e precisa ver o status da nota na hora. O POST /v1/monitoramento foi feito pra isso — ele consulta a SEFAZ de forma síncrona e já devolve o resultado junto da confirmação.

Não precisa de segunda chamada. O campo status_atual da resposta 201 já vem preenchido com o que a SEFAZ respondeu naquele instante (autorizada, cancelada, denegada, inexistente). Fazer um GET logo em seguida é desperdício de rede — e pode devolver o mesmo valor antes de qualquer novo ciclo do worker.
A exceção: status_atual pode vir null. Quando a SEFAZ da UF não responde dentro do orçamento de 12 s, preferimos devolver o 201 na hora a segurar sua conexão até estourar o timeout. A chave já está monitorada — o status atualiza no ciclo seguinte, em até 5 min: consulte GET /v1/monitoramento/{chave}. Na tela, trate como “em monitoramento, status em instantes”, nunca como erro.

Tempo de resposta e timeout

Mediana~140 msconsulta direta ao webservice da UF
p95~500 ms95% das chamadas terminam abaixo disso
Teto do servidor12 spassou disso, respondemos 201 com status_atual: null
Timeout sugerido30 sfolga confortável sobre o teto de 12 s

Na prática um spinner simples resolve — não vale montar fluxo assíncrono pra meio segundo. O teto de 12 s é nosso, não seu: quando a SEFAZ da UF está lenta, desistimos da consulta síncrona e devolvemos a confirmação mesmo assim, em vez de deixar seu ERP pendurado. Com um timeout de 30 s no seu lado, a chamada nunca deveria estourar por nossa causa.

O que o botão precisa tratar

HTTPSignificaNa tela
201monitorada + consultada agoramostrar status_atual
409já estava em monitoramentotratar como sucesso — “já monitorada”
400chave inválida ou modelo não suportadovalidar antes de enviar
402limite do plano atingidoavisar o responsável pela conta
503indisponibilidade momentâneapermitir tentar de novo
O 409 não é erro e não cobra de novo. Se o usuário clicar duas vezes, ou se a rotina reenviar notas já acompanhadas, a resposta é 409 e nenhuma unidade é consumida. Mostrar isso como falha gera chamado de suporte à toa. A única resposta 2xx para nota que já era vigiada é a promoção de uma chave descoberta automaticamente (promovida: true), que é cobrada.
botao-monitorar.tstypescript
// Botão "monitorar" no ERP — usuário aguardando na tela.
// A resposta 201 já traz o status consultado na SEFAZ; não faça um GET depois.
async function monitorar(chaveAcesso: string) {
  const ctrl = new AbortController();
  // 30s: p95 real é ~500ms, mas UF lenta pode cair na contingência.
  const t = setTimeout(() => ctrl.abort(), 30_000);

  try {
    const res = await fetch("https://api.vigiaxml.com/v1/monitoramento", {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.VIGIA_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        chaveAcesso,
        callbackUrl: "https://erp.suaempresa.com.br/webhooks/vigiaxml",
      }),
      signal: ctrl.signal,
    });

    // 409 = já estava monitorada. Não é erro e NÃO cobra de novo.
    if (res.status === 409) return { ok: true, aviso: "Nota já monitorada." };
    // 200 com promovida: true = era vigiada por descoberta automática (grátis)
    // e agora virou monitoramento contratado — cobrado (R$ 0,35).

    if (res.status === 402) return { ok: false, erro: "Limite do plano atingido." };
    if (res.status === 400) return { ok: false, erro: "Chave inválida." };
    if (!res.ok) return { ok: false, erro: "Indisponível — tente novamente." };

    const data = await res.json();
    // status_atual: autorizada | cancelada | denegada | inexistente | null
    // null = SEFAZ não respondeu em 12s. A nota ESTÁ monitorada; o status
    // atualiza no ciclo seguinte (~5 min) — confirme com GET /v1/monitoramento.
    // Mostre "aguardando SEFAZ", não erro.
    return { ok: true, status: data.status_atual, expiraEm: data.expira_em };
  } catch (e) {
    if ((e as Error).name === "AbortError") {
      // O monitoramento pode ter sido criado mesmo com timeout no cliente:
      // confirme com GET /v1/monitoramento/:chave antes de reenviar.
      return { ok: false, erro: "Tempo esgotado — verifique antes de repetir." };
    }
    return { ok: false, erro: "Falha de rede." };
  } finally {
    clearTimeout(t);
  }
}
Uma chave por vez, não lote. Este endpoint é pra ação individual com usuário esperando. Pra carga em volume (centenas ou milhares de chaves de uma vez), use POST /v1/importar com pularConsultaInline=true e deixe o worker fazer as consultas — requisição síncrona não é o lugar pra isso.
Endpoint

Histórico de consultas

GET/v1/consultas

Lista paginada de todas as consultas que sua carteira já fez (avulsas + as do monitoramento). Útil pra reconciliação caso o webhook tenha ficado off ou pra auditoria/relatório.

Query params

ParamTipoDefaultDescrição
chavestring—Filtrar por uma chave específica (44 caracteres).
sinceISO 8601—Apenas consultas com queried_at ≥ valor.
limitint (1-200)50Tamanho da página.
offsetint0Pular N resultados (paginação simples).
de / ate / cursorISO 8601 / string—Ativam o modo feed: ordem ascendente, até 500 por página, next_cursor no lugar de total. Não combinam com offset/since. Ver Sincronização incremental.
consultas.shbash
# Histórico paginado de consultas (mais recentes primeiro).
# Filtros: chave, since (ISO 8601), limit (max 200), offset.
# Pra espelhar no seu banco, use o modo feed (de/ate/cursor) — ver Sincronização.
curl "https://api.vigiaxml.com/v1/consultas?since=2026-05-01T00:00:00Z&limit=50" \
  -H "X-Api-Key: $VIGIA_API_KEY"
Endpoint

Eventos vinculados

GET/v1/eventos

Lista eventos que vieram acoplados às consultas: CCe (110110), Cancelamento (110111), Manifestação Destinatário (210200/210210/210220/210240), Passagem (310620 CT-e), Encerramento (110112 MDF-e), e outros. Use tp=210240 pra detectar duplicatas frias (destinatário declarando que não fez a operação).

eventos.shbash
# Eventos vinculados (CCe, manifestação, passagem, encerramento, etc).
# Filtros: chave, tp (tipo do evento), since, limit, offset.
# Pra espelhar no seu banco, use o modo feed (de/ate/cursor) — ver Sincronização.
curl "https://api.vigiaxml.com/v1/eventos?tp=210240&since=2026-05-01T00:00:00Z" \
  -H "X-Api-Key: $VIGIA_API_KEY"

Resposta

response.jsonjson
{
  "total": 2,
  "offset": 0,
  "limit": 50,
  "items": [
    {
      "id": "uuid",
      "chave_acesso": "35...",
      "tp_evento": "210240",
      "descricao": "Operação não Realizada",
      "n_seq_evento": 1,
      "dh_evento": "2026-05-04T18:21:09Z",
      "dh_reg_evento": "2026-05-04T18:21:11Z",
      "cnpj_autor": "12345678000190",
      "c_orgao": "35",
      "detalhe": "duplicata fria (manifestação 210240)",
      "protocolo": "135260000123456",
      "consulta_id": "uuid-da-consulta-que-capturou",
      "captured_at": "2026-05-04T19:00:00Z",
      "atualizado_em": "2026-05-04T19:00:00Z",
      "fonte_detalhe": null
    }
  ]
}
Cobertura

O que cada SEFAZ devolve, por UF

Consultamos a SEFAZ autorizadora de cada documento (a do estado do emitente; 15 estados usam a SEFAZ Virtual do RS, a SVRS). A consulta de situação devolve o status e os eventos registrados naquela SEFAZ — cancelamento, CCe, EPEC — em todas as UFs. Os eventos registrados no Ambiente Nacional — manifestação do destinatário (ciência, confirmação, desconhecimento, operação não realizada) e o vínculo “CT-e/MDF-e autorizado” na NF-e — só chegam onde a SEFAZ os sincroniza. Medido numa carteira de ~480 mil chaves em 30 dias (set/2026); o comportamento das SEFAZ pode mudar sem aviso.

UFAutorizadoraCancelamento / CCeManifestação do destinatárioVínculo NF-e → CT-e/MDF-eEventos de transporte (CT-e/MDF-e)Consulta cadastral (pool)
SPprópria✅✅✅✅❌ cert. próprio
MSprópria✅✅✅✅❌ cert. próprio
AMprópria✅✅✅✅sem serviço
MGprópria✅❌❌❌ não devolve eventos de CT-e❌ cert. próprio
PRprópria✅❌❌✅✅
GOprópria✅❌❌✅✅
BAprópria✅❌❌✅❌ cert. próprio
MTprópria✅❌❌✅❌ cert. próprio
PEprópria✅❌❌✅❌ cert. próprio
CEprópria✅❌❌✅❌ cert. próprio
PAprópria✅❌❌✅❌ cert. próprio
MASVAN✅❌❌✅❌ cert. próprio
SC, RS, RJ, ES, DF, PB, AL, TO, RN, SE, PI, RO, ACSVRS✅❌❌✅❌ cert. próprio
AP, RRSVRS✅❌ (presumido)❌ (presumido)✅❌ cert. próprio
Como ler. Numa NF-e de SP você recebe a manifestação e o CT-e vinculado; numa NF-e de SC você recebe status, cancelamento e CCe — a manifestação e o CT-e existem no portal nacional, mas a SVRS não os expõe (o portal da própria SEFAZ-SC também não mostra). Para CT-e e MDF-e os eventos de transporte chegam em todas as UFs, exceto MG. O caminho previsto pelo fisco para um terceiro enxergar o Ambiente Nacional é ser registrado como ator interessado (evento 410301) pelo emitente — fale com a gente se esse for o seu caso.
Endpoint

Sincronização incremental

GET/v1/consultas?cursor=GET/v1/eventos?cursor=GET/v1/consultas/resumoGET/v1/eventos/resumo

Pra manter uma cópia das consultas e dos eventos no seu banco. Os mesmos endpoints de histórico entram no modo feed quando o request traz de, ate ou cursor: ordem ascendente, páginas de até 500 e um next_cursor que diz de onde continuar. O estado da sincronização é uma string guardada do seu lado — se o seu job ficar fora por uma semana, ele continua do mesmo ponto sem perder nada. É o desenho certo pra espelhar dados; webhook não é.

Query params

ParamTipoDefaultDescrição
deISO 8601inícioInício da janela, inclusivo. Só ISO 8601 (2026-09-08, 2026-09-08T10:00:00, …-03:00; o + na query vai como %2B). Sem offset, ou só a data, vale o horário de Brasília. Outro formato (08/09/2026) é recusado com 400, não adivinhado.
ateISO 8601agora − 10 minFim da janela, exclusivo. Um valor no futuro é ajustado pro teto de segurança.
cursorstring—Continuação: mande o next_cursor da resposta anterior, e só ele (já carrega a janela e os filtros chave/tp da primeira chamada). cursor= vazio = começar do início.
limitint (1-500)500Tamanho da página.

O relógio da janela é queried_at nas consultas e atualizado_em nos eventos. Combinações inválidas (cursor com de/ate, offset/since no modo feed, de ≥ ate) respondem 400 com error e hint.

sync.shbash
# Primeira chamada: cursor vazio = começar do início.
curl "https://api.vigiaxml.com/v1/consultas?cursor=&limit=500" \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Chamadas seguintes: só o next_cursor da resposta anterior.
curl "https://api.vigiaxml.com/v1/consultas?cursor=eyJmIjoiY29uc3VsdGFzIiwidCI6Ii4uLiJ9" \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Ou por janela de data (de inclusivo, ate exclusivo; sem hora = meia-noite BRT).
curl "https://api.vigiaxml.com/v1/consultas?de=2026-09-08&ate=2026-09-09" \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Eventos: mesmo contrato, cursor próprio.
curl "https://api.vigiaxml.com/v1/eventos?cursor=&limit=500" \
  -H "X-Api-Key: $VIGIA_API_KEY"

# Conferência: total por status na janela (máx. 7 dias).
curl "https://api.vigiaxml.com/v1/consultas/resumo?de=2026-09-08&ate=2026-09-09" \
  -H "X-Api-Key: $VIGIA_API_KEY"

Resposta

response.jsonjson
{
  "items": [
    {
      "id": "b7e3a9f0-4c21-4f8a-9d3e-1a2b3c4d5e6f",
      "chave_acesso": "35260912345678000190550010000123451000123450",
      "modelo": 55,
      "uf": "SP",
      "status": "cancelada",
      "c_stat": "101",
      "x_motivo": "Cancelamento de NF-e homologado",
      "protocolo_sefaz": "135260001240001",
      "data_status_sefaz": "2026-09-08T18:47:33+00:00",
      "queried_at": "2026-09-08T19:00:07+00:00",
      "origem": "Monitoramento",
      "alerta_manifestacao": false,
      "tem_cce": false
    }
  ],
  "next_cursor": "eyJmIjoiY29uc3VsdGFzIiwidCI6IjIwMjYtMDktMDhUMTk6MDA6MDcrMDA6MDAiLCJpIjoiYjdlMy4uLiJ9",
  "has_more": false
}

Job de exemplo

sync.pypython
# roda num cron a cada 5 minutos
def sincronizar(feed):                      # "consultas" ou "eventos"
    cursor = db.get_cursor(feed) or ""      # "" na primeira vez
    while True:
        r = http.get(f"https://api.vigiaxml.com/v1/{feed}",
                     params={"cursor": cursor, "limit": 500},
                     headers={"X-Api-Key": API_KEY}).json()
        for item in r["items"]:
            db.upsert(feed, item)           # chave do upsert: item["id"]
        cursor = r["next_cursor"]
        db.save_cursor(feed, cursor)        # só DEPOIS do upsert commitar
        if not r["has_more"]:
            break

sincronizar("consultas")
sincronizar("eventos")

Regras

  • Upsert por id. A entrega é at-least-once: janelas sobrepostas e empates de relógio podem repetir um item; com upsert isso é inofensivo.
  • Avance o cursor só depois de gravar a página. Se o job morrer no meio, a próxima rodada repete a página.
  • Cursores independentes pra consultas e eventos.
  • Janela de segurança: só saem linhas com relógio anterior a agora − 10 min, porque a gravação pode commitar minutos depois do relógio em lotes grandes.
  • Rate limit próprio: feeds e /resumo têm 120 req/min por API key, separados do limite das consultas — o backfill não concorre com a sua operação.
  • Eventos enriquecidos reaparecem. Quando um evento ganha detalhe (por exemplo via upload de XML), atualizado_em avança e ele volta no feed; captured_at não muda. O upsert aplica.
  • Erro (429, 5xx, timeout): não avance o cursor; deixe o próximo ciclo repetir. A posição está no cursor, não no tempo.
  • Conferência: /resumo com de e ate obrigatórios (máx. 7 dias; aceita chave/tp) devolve total e por_status / por_tp_evento — compare com o count do seu banco.
O que o feed não é. Não é tempo real: reflete a cadência do monitoramento (hora em hora no dia da inclusão, diária depois). Eventos vêm de carona na consulta de status — só de chaves consultadas, e só os que a SEFAZ devolve nela (varia por UF: veja Cobertura por UF). Não há exclusões. E fonte_detalhe: "upload_cliente" marca detalhe enviado pelo próprio tenant sem verificação criptográfica — trate diferente de null (SEFAZ).
Endpoint

Rastreio cross-document

GET/v1/rastreio/:chave

Dado uma chave (NF-e, CT-e ou MDF-e), devolve a árvore completa de documentos vinculados via eventos + linha do tempo cronológica. Útil pra reconstruir a história fiscal de uma operação: NF-e → CT-e (transportadora) → MDF-e (encerramento + condutor). Restrito ao escopo do tenant — não vaza chaves de outras carteiras.

rastreio.shbash
# Árvore de documentos vinculados (NF-e ↔ CT-e ↔ MDF-e) com timeline.
# Use pra reconstruir a história completa de uma operação fiscal.
curl https://api.vigiaxml.com/v1/rastreio/35240300000000000000550010000001231123456785 \
  -H "X-Api-Key: $VIGIA_API_KEY"
É aqui que ficam os eventos. Se você quer saber o que aconteceu com a nota — manifestação, passagem em pedágio, entrega, encerramento — o campo timeline deste endpoint é o lugar. O GET /v1/monitoramento devolve apenas a situação da carteira (status_atual, prazos), nunca eventos.
O link do mapa já vem junto. A resposta traz link_rastreio com a URL da tela de trajeto, pronta pra mandar ao usuário final — sem chamada extra e sem exigir login de quem abre. Use ?linkDias=15 pra mudar a validade (padrão 7, teto 30), ou ?linkRastreio=false se preferir não recebê-lo.
Webhooks

Notificações pra seu endpoint

Dois eventos: mudança de status de um monitoramento (para o callbackUrl cadastrado nele) e conclusão de job de importação em lote. Timeout: 10s. Retry com backoff (5min, 10min, 15min) após falha.

Eventos disponíveis hoje

event_typeQuando dispara
webhook.status_mudouO status de uma chave em monitoramento mudou (ex.: autorizada → cancelada). Não dispara na primeira consulta nem em falha de comunicação com a SEFAZ — só quando a SEFAZ devolve um status diferente do anterior. 1 entrega + até 3 retentativas.
job.concluidoJob de importação em lote finalizou. Inclui contadores de sucesso/erro e duração total.

Mudança de status — requisição completa

webhook-status.httphttp
POST https://erp.example.com/webhooks/vigia
Content-Type: application/json
User-Agent: VigiaXML-Webhook/1.0
X-VigiaXML-Event: webhook.status_mudou
X-VigiaXML-Delivery: 9b2f4c1e-6a3d-4f8e-b1c7-2d5e8a0f4b93

{
  "event_type": "webhook.status_mudou",
  "chave_acesso": "35240300000000000000550010000001231123456785",
  "monitoramento_id": "c7a1e2d4-5b6f-4a8c-9d0e-1f2a3b4c5d6e",
  "status_anterior": "autorizada",
  "status_novo": "cancelada",
  "c_stat": "101",
  "x_motivo": "Cancelamento de NF-e homologado",
  "ocorrido_em": "2026-10-03T14:05:12.345+00:00"
}

status_anterior e status_novo usam os mesmos valores do campo status da API (autorizada, cancelada, inexistente, encerrada…; denegada só em notas anteriores a set/2024). O webhook é um aviso: a fonte da verdade continua sendo GET /v1/monitoramento/:chave e o feed de GET /v1/consultas.

Conclusão de lote — payload

webhook.jsonjson
{
  "event_type": "job.concluido",
  "job_id": "5e1d8a64-2c4f-4a3a-9ae8-5a0fbb2c9871",
  "tenant_id": "uuid-da-carteira",
  "tipo": "importar",
  "total_chaves": 200,
  "sucesso": 198,
  "erro": 2,
  "started_at": "2026-05-04T20:31:00Z",
  "finished_at": "2026-05-04T20:33:18Z",
  "duration_seconds": 138.4
}

Idempotência

Mudança de status: use o header X-VigiaXML-Delivery, igual em todas as tentativas. Lote: use job_id. O mesmo evento pode chegar 2x se sua resposta demorar > 10s.

Retry

Não-2xx ou timeout (10s) → backoff em minutos (5, 10, 15). Depois disso desistimos. Endpoint que não responde num ciclo não é chamado de novo naquele ciclo — as entregas pendentes dele contam a falha e seguem o backoff.

Roadmap: assinatura HMAC-SHA256 do payload está planejada, sem data pública. Até lá, inclua um token secreto na própria URL do callback (ex.: ?token=…) e confira no seu endpoint.
Erros

Códigos e como reagir

Toda resposta de erro é JSON com pelo menos o campo error:

error.jsonjson
{
  "error": "chave_acesso deve ter 44 caracteres"
}
CasoHTTPSignificadoO que fazer
invalid_request400Payload malformado, campo faltando ou chave de acesso inválida.Confira o body. Chave deve ter exatamente 44 caracteres.
unauthenticated401Header X-Api-Key ausente, mal formatado ou revogado.Verifique o header. Gere uma chave nova em /conta/api-key se necessário.
trial_expired402Trial gratuito expirou (30 dias) ou bateu o teto de 5.000 consultas.Contate vendas pra liberar plano pago.
not_found404Chave não está em monitoramento nessa carteira ou recurso não existe.Verifique se a chave foi de fato adicionada ao seu tenant.
conflict409Chave já está em monitoramento ativo nessa carteira.Use GET /v1/monitoramento/:chave pra ver o estado existente.
modelo_nao_suportado400Modelo da chave (posições 21-22) não está em (55, 57, 58, 67).Hoje suportamos NF-e (55), CT-e (57), CT-e OS (67) e MDF-e (58).
sefaz_unavailable503Pool de certificados sem capacidade ou SEFAZ off.Aguarde alguns minutos e tente novamente.
internal_error500Falha interna do VigiaXML.Repetir com backoff. Persistindo, escreva para suporte.
Códigos da SEFAZ

cStat: o que a SEFAZ respondeu

As respostas de consulta trazem o c_stat e o x_motivo devolvidos pela SEFAZ, além do status já normalizado. A regra de ouro: separe o que a SEFAZ disse sobre a nota do que é falha de consulta — falha nunca vira status, só pede nova tentativa.

Digite o número (por exemplo, 217 ou “rejeição 656”) ou uma palavra (por exemplo, “cancelamento”). As mensagens estão resumidas; o texto oficial pode variar.

19 códigos encontrados

  • 100Autorizado o uso da NF-eSituação da nota

    A nota está autorizada neste momento.

    O que fazer: Siga com a análise. Autorizada hoje não quer dizer autorizada amanhã: o emitente pode cancelar a nota (o prazo regular é de 24 horas após a autorização, e há cancelamentos fora desse prazo).

  • 150Autorizado o uso da NF-e, autorização fora de prazoSituação da nota

    A nota está autorizada, mas foi transmitida mais de 7 dias depois da data de emissão. Acontece em contingência e em nota emitida com data retroativa.

    O que fazer: Trate como autorizada, mas compare a data de emissão com a do protocolo: nota com data retroativa como lastro de duplicata merece uma pergunta ao cedente.

  • 120Autorizado o uso da NF-e, com alertaSituação da nota

    A nota está autorizada e gravada na SEFAZ, que só acrescentou um alerta na resposta (NT 2026.002, em produção desde 05/10/2026). Por enquanto vale só para NFC-e.

    O que fazer: Trate como autorizada. O alerta vem junto na resposta da SEFAZ; hoje o único previsto é o de CNPJ do destinatário inabilitado.

  • 101Cancelamento de NF-e homologadoSituação da nota

    O emitente cancelou a nota. Ela não tem mais validade.

    O que fazer: Não antecipe. Se a duplicata já foi antecipada, acione o cedente pelo que o contrato prevê (recompra ou substituição do título).

  • 151 / 155Cancelamento homologado fora de prazoSituação da nota

    A nota foi cancelada depois do prazo regular de 24 horas, com aceite da SEFAZ.

    O que fazer: O mesmo do 101: a nota não vale. Cancelamento tardio é um sinal adicional de risco sobre o cedente.

  • 217NF-e não consta na base de dados da SEFAZSituação da nota

    A SEFAZ não encontrou a nota com essa chave.

    O que fazer: Confira a chave posição a posição. Se a nota acabou de ser emitida, consulte de novo em algumas horas; se a DANFE indica emissão em contingência, o emitente tem até 7 dias (168 horas) para transmiti-la. Se continuar sem constar depois disso, não aceite.

  • 110Uso denegadoSituação da nota

    Até setembro de 2024, a SEFAZ negava o uso da NF-e por irregularidade fiscal do emitente ou do destinatário. Desde 02/09/2024 a denegação de NF-e não existe mais: o problema gera rejeição e a nota não chega a ser registrada.

    O que fazer: Só aparece em notas antigas. Nota denegada não serve de lastro.

  • 526Consulta a uma chave de acesso muito antigaConsulta não concluída

    O mês de emissão da chave tem mais de 6 meses, e o serviço automático de consulta não responde mais por ela. Não indica problema na nota.

    O que fazer: Consulte pelo Portal da NF-e ou use o XML com o protocolo de autorização. Para antecipação, uma nota tão antiga já é, por si, um ponto a investigar.

  • 108Serviço paralisado momentaneamente (curto prazo)Consulta não concluída

    Código do serviço de status da SEFAZ: a SEFAZ da UF está fora do ar por pouco tempo. Nada se sabe sobre a nota.

    O que fazer: Repita a consulta mais tarde. Nunca registre a nota como inválida por causa disso.

  • 109Serviço paralisado sem previsãoConsulta não concluída

    Código do serviço de status da SEFAZ: a SEFAZ da UF está fora do ar sem previsão de volta. Nada se sabe sobre a nota.

    O que fazer: Repita mais tarde. Não tire conclusão sobre a nota.

  • 656Consumo indevidoConsulta não concluída

    Consultas demais em pouco tempo: a SEFAZ bloqueou temporariamente quem está consultando. Não tem relação com a nota.

    O que fazer: Pare de consultar por um tempo (a duração do bloqueio varia por SEFAZ) e reduza a frequência das chamadas.

  • 410UF informada não é atendida pelo web serviceConsulta não concluída

    A consulta foi enviada ao serviço errado para a UF da nota. Não diz nada sobre ela.

    O que fazer: Erro do sistema que fez a consulta: avise o fornecedor ou a equipe de TI.

  • 280–286Problemas com o certificado digital da consultaConsulta não concluída

    O certificado usado na consulta foi recusado: inválido, vencido, revogado ou com problema na cadeia. O 286 (falha no acesso à lista de revogação) costuma ser problema do lado da SEFAZ. Nada disso diz algo sobre a nota.

    O que fazer: Verifique o certificado; no 286, repita mais tarde.

  • 999Erro não catalogadoConsulta não concluída

    Falha interna da SEFAZ sem código específico. Nada se sabe sobre a nota.

    O que fazer: Repita mais tarde.

  • 135Evento registrado e vinculado à NF-eEventos

    Um evento (cancelamento, carta de correção, manifestação etc.) foi registrado na nota.

    O que fazer: Veja qual evento foi: é o tipo do evento, e não este código, que diz se há risco.

  • 111Consulta cadastro com uma ocorrênciaConsulta cadastral

    O contribuinte foi encontrado no cadastro da UF, com um registro.

    O que fazer: Confira a situação da Inscrição Estadual (habilitada ou não) no retorno.

  • 112Consulta cadastro com mais de uma ocorrênciaConsulta cadastral

    O contribuinte tem mais de um registro na UF (por exemplo, mais de uma Inscrição Estadual).

    O que fazer: Olhe todas as ocorrências: uma inscrição baixada não significa que a empresa inteira esteja irregular.

  • 259CNPJ da consulta não cadastrado como contribuinte na UFConsulta cadastral

    A empresa não tem Inscrição Estadual na UF consultada — o que vale só para essa UF; ela pode ser contribuinte em outra.

    O que fazer: Para quem emite NF-e de venda de mercadoria a partir dessa UF, é um sinal de alerta que merece explicação.

  • 257Solicitante não habilitado para emissão da NF-eConsulta cadastral

    A UF só atende a consulta cadastral de certificados de emissores de NF-e cadastrados nela. Não diz nada sobre a empresa consultada.

    O que fazer: Consulte com um certificado habilitado na UF ou por outro canal oficial da SEFAZ local.

Rate limits

Limites por carteira

Limite por API key. O default é de 60 req/min (configurável em RateLimitConsultasMin). Volume maior é negociado — fale com vendas.

Trial: além do rate limit, contas em trial (signup self-service) têm teto adicional de 5.000 consultas em 30 dias. Quando bate, a API devolve 402 trial_expired até o admin liberar plano pago.

Pronto para o primeiro deploy?

Cadastre-se em 1 minuto, confirme o email e dispare a primeira consulta sem cartão de crédito. Trial de 30 dias / 5.000 consultas.