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 dígitos. Aceita com hífens/espaços (são removidos).
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 nunca teve inscrição nesta UF” também é informação. 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 dígitos. Modelos aceitos: 55, 57, 58, 67.
callbackUrlstring (https)Opcional. Aceito e armazenado para as futuras notificações de mudança de status (em desenvolvimento — ainda não dispara). Hoje, acompanhe via GET /v1/monitoramento/:chave.
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.
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.
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." };

    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
chavestringFiltrar por uma chave específica (44 dígitos).
sinceISO 8601Apenas consultas com queried_at ≥ valor.
limitint (1-200)50Tamanho da página.
offsetint0Pular N resultados (paginação simples).
de / ate / cursorISO 8601 / stringAtivam 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ópriasem 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.
cursorstringContinuaçã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

Hoje o único evento entregue é o de conclusão de job de importação em lote (assíncrono). Timeout: 10s. Retry: até 3 tentativas com backoff (5min, 10min, 15min).

Eventos disponíveis hoje

event_typeQuando dispara
job.concluidoJob de importação em lote finalizou. Inclui contadores de sucesso/erro e duração total.
Mudança de status de monitoramento ainda não gera webhook — está em desenvolvimento. O callbackUrl que você enviar fica armazenado e passará a receber automaticamente quando o evento existir. Até lá, acompanhe por GET /v1/monitoramento/:chave, GET /v1/consultas ou GET /v1/rastreio/:chave.

Payload de exemplo

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

Use job_id como chave de dedup. Mesmo job pode chegar 2x se sua resposta demorar > 10s.

Retry

Não-2xx ou timeout (10s) → backoff em minutos (5, 10, 15). Após 3 tentativas desistimos — o erro fica visível no painel.

Roadmap: webhook por mudança de status no monitoramento (nota.status_alterado) e assinatura HMAC-SHA256 do payload estão planejados, sem data pública. Hoje, valide a fonte por IP allowlist ou token na URL.
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 dígitos"
}
CasoHTTPSignificadoO que fazer
invalid_request400Payload malformado, campo faltando ou chave de acesso inválida.Confira o body. Chave deve ter exatamente 44 dígitos.
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.
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.