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.
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.
# 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"
}'Sign-up self-service e 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.
# 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"202 com a mesma mensagem, mesmo se o email/CNPJ já existir. Atacantes não conseguem mapear sua base de clientes.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.
Consulta avulsa
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
| Campo | Tipo | Descrição |
|---|---|---|
| chaveAcesso | string (obrig.) | Chave de acesso de 44 dígitos. Aceita com hífens/espaços (são removidos). |
curl -X POST https://api.vigiaxml.com/v1/consulta \
-H "X-Api-Key: $VIGIA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"chaveAcesso": "35240300000000000000550010000001231123456785"
}'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_idResposta 200
{
"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"
}Consulta cadastral do contribuinte
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.
# 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"{
"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
}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.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.Importação em lote
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).
# 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
{
"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 }
]
}"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.Monitoramento contínuo (30 dias)
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).
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
| Campo | Tipo | Descrição |
|---|---|---|
| chaveAcesso | string (obrig.) | Chave de acesso de 44 dígitos. Modelos aceitos: 55, 57, 58, 67. |
| callbackUrl | string (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. |
# 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)
{
"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"
}409 conflict com o monitoramento existente. Use GET /v1/monitoramento/:chave se precisar do estado atual.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.
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.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 ms | consulta direta ao webservice da UF |
| p95 | ~500 ms | 95% das chamadas terminam abaixo disso |
| Teto do servidor | 12 s | passou disso, respondemos 201 com status_atual: null |
| Timeout sugerido | 30 s | folga 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
| HTTP | Significa | Na tela |
|---|---|---|
| 201 | monitorada + consultada agora | mostrar status_atual |
| 409 | já estava em monitoramento | tratar como sucesso — “já monitorada” |
| 400 | chave inválida ou modelo não suportado | validar antes de enviar |
| 402 | limite do plano atingido | avisar o responsável pela conta |
| 503 | indisponibilidade momentânea | permitir tentar de novo |
// 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);
}
}
POST /v1/importar com pularConsultaInline=true e deixe o worker fazer as consultas — requisição síncrona não é o lugar pra isso.Link de rastreio pro usuário final
Devolve uma URL pronta com a tela de rastreio — situação na SEFAZ, linha do tempo e o mapa do trajeto do caminhão. Quem abre não precisa de conta nem de login: a autorização está assinada dentro do próprio link. Serve pra colar num e-mail, num WhatsApp ou num botão “acompanhar” do seu sistema.
linkRastreio: true no POST /v1/monitoramento ou no POST /v1/importar e o link já volta na resposta que você consome de qualquer jeito — sem chamada extra. Use este endpoint só pra gerar um link novo pra uma nota antiga, ou pra renovar um que expirou.Já na resposta que você usa
# Uma nota, com link já na resposta
curl -X POST https://api.vigiaxml.com/v1/monitoramento \
-H "X-Api-Key: $VIGIAXML_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "chaveAcesso": "'$CHAVE'", "linkRastreio": true, "linkDias": 15 }'
# 201 Created
# {
# "success": true,
# "link_rastreio": "https://vigiaxml.com/rastreio/a1b2...c3d4",
# "link_expira_em": "2026-08-19T18:00:00+00:00",
# "status_atual": "autorizada",
# ...
# }
# Em lote: cada item de "results" volta com o seu link.
curl -X POST https://api.vigiaxml.com/v1/importar \
-H "X-Api-Key: $VIGIAXML_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "chaves": ["...", "..."], "modo": "monitoramento", "linkRastreio": true }'
# { "results": [ { "chave": "...", "status": "autorizada",
# "link_rastreio": "https://vigiaxml.com/rastreio/..." } ] }Ou avulso, pra uma nota já consultada
# Gera um link de rastreio válido por 15 dias
curl -X POST "https://api.vigiaxml.com/v1/rastreio/$CHAVE/link?dias=15" \
-H "X-Api-Key: $VIGIAXML_API_KEY"
# 200 OK
# {
# "url": "https://vigiaxml.com/rastreio/a1b2...c3d4",
# "chave": "35260344340879000196550010001073051136511817",
# "expira_em": "2026-08-19T18:00:00+00:00"
# }
#
# Mande essa url pro cliente final. Ela abre sozinha, sem login.| dias | validade do link | opcional · padrão 7 · máximo 30 |
| 404 | chave não consultada por esta conta | monitore ou consulte a chave antes de gerar o link |
Histórico de 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
| Param | Tipo | Default | Descrição |
|---|---|---|---|
| chave | string | — | Filtrar por uma chave específica (44 dígitos). |
| since | ISO 8601 | — | Apenas consultas com queried_at ≥ valor. |
| limit | int (1-200) | 50 | Tamanho da página. |
| offset | int | 0 | Pular N resultados (paginação simples). |
| de / ate / cursor | ISO 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. |
# 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"Eventos vinculados
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 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
{
"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
}
]
}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.
| UF | Autorizadora | Cancelamento / CCe | Manifestação do destinatário | Vínculo NF-e → CT-e/MDF-e | Eventos de transporte (CT-e/MDF-e) | Consulta cadastral (pool) |
|---|---|---|---|---|---|---|
| SP | própria | ✅ | ✅ | ✅ | ✅ | ❌ cert. próprio |
| MS | própria | ✅ | ✅ | ✅ | ✅ | ❌ cert. próprio |
| AM | própria | ✅ | ✅ | ✅ | ✅ | sem serviço |
| MG | própria | ✅ | ❌ | ❌ | ❌ não devolve eventos de CT-e | ❌ cert. próprio |
| PR | própria | ✅ | ❌ | ❌ | ✅ | ✅ |
| GO | própria | ✅ | ❌ | ❌ | ✅ | ✅ |
| BA | própria | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| MT | própria | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| PE | própria | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| CE | própria | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| PA | própria | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| MA | SVAN | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| SC, RS, RJ, ES, DF, PB, AL, TO, RN, SE, PI, RO, AC | SVRS | ✅ | ❌ | ❌ | ✅ | ❌ cert. próprio |
| AP, RR | SVRS | ✅ | ❌ (presumido) | ❌ (presumido) | ✅ | ❌ cert. próprio |
Sincronização incremental
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
| Param | Tipo | Default | Descrição |
|---|---|---|---|
| de | ISO 8601 | início | Iní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. |
| ate | ISO 8601 | agora − 10 min | Fim da janela, exclusivo. Um valor no futuro é ajustado pro teto de segurança. |
| cursor | string | — | 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. |
| limit | int (1-500) | 500 | Tamanho 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.
# 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
{
"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
# 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
/resumotê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_emavança e ele volta no feed;captured_atnã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:
/resumocomdeeateobrigatórios (máx. 7 dias; aceitachave/tp) devolvetotalepor_status/por_tp_evento— compare com o count do seu banco.
fonte_detalhe: "upload_cliente" marca detalhe enviado pelo próprio tenant sem verificação criptográfica — trate diferente de null (SEFAZ).Rastreio cross-document
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.
# Á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"timeline deste endpoint é o lugar. O GET /v1/monitoramento devolve apenas a situação da carteira (status_atual, prazos), nunca eventos.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.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_type | Quando dispara |
|---|---|
| job.concluido | Job de importação em lote finalizou. Inclui contadores de sucesso/erro e duração total. |
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
{
"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.
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.Códigos e como reagir
Toda resposta de erro é JSON com pelo menos o campo error:
{
"error": "chave_acesso deve ter 44 dígitos"
}| Caso | HTTP | Significado | O que fazer |
|---|---|---|---|
| invalid_request | 400 | Payload malformado, campo faltando ou chave de acesso inválida. | Confira o body. Chave deve ter exatamente 44 dígitos. |
| unauthenticated | 401 | Header X-Api-Key ausente, mal formatado ou revogado. | Verifique o header. Gere uma chave nova em /conta/api-key se necessário. |
| trial_expired | 402 | Trial gratuito expirou (30 dias) ou bateu o teto de 5.000 consultas. | Contate vendas pra liberar plano pago. |
| not_found | 404 | Chave não está em monitoramento nessa carteira ou recurso não existe. | Verifique se a chave foi de fato adicionada ao seu tenant. |
| conflict | 409 | Chave já está em monitoramento ativo nessa carteira. | Use GET /v1/monitoramento/:chave pra ver o estado existente. |
| modelo_nao_suportado | 400 | Modelo 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_unavailable | 503 | Pool de certificados sem capacidade ou SEFAZ off. | Aguarde alguns minutos e tente novamente. |
| internal_error | 500 | Falha interna do VigiaXML. | Repetir com backoff. Persistindo, escreva para suporte. |
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.