Verificação de NF-e
Verificação em dois níveis de NF-e de terceiros: verificação de XML, consulta por chave, veredito final entregue por webhook, idempotência e modelo de erros.
Visão geral
A plataforma executa uma verificação em dois níveis de NF-e de terceiros (modelo 55):
| Nível | O que faz | Quando |
|---|---|---|
| Nível 1 (síncrono) | Validação local do XML: sintaxe, layout XSD da NF-e 4.00, assinatura digital e titularidade do certificado, estrutura da chave de acesso de 44 dígitos e coerência com os campos do documento, integridade do protocolo de autorização | Devolvido imediatamente na resposta da API |
| Nível 2 (assíncrono) | Verificação de autenticidade em tempo real na SEFAZ (a autoridade fiscal estadual): situação da autorização, eventos de cancelamento, número do protocolo e digest comparados com os registros oficiais | Executa depois que o nível 1 é aprovado; o veredito é entregue por webhook (veja Recebendo o veredito final), e a consulta por chave devolve o estado atual como alternativa de polling |
Uma consulta por chave independente devolve os dados da nota pela chave de acesso. Para uma chave que você enviou pela verificação de XML, ela também devolve o bloco validation atual e serve como alternativa de polling quando uma entrega de webhook se perde.
Os dois endpoints exigem os três cabeçalhos de assinatura, veja Autenticação. As respostas são objetos simples (sem envelope da plataforma); erros de requisição são um objeto simples {code, message}.
Ciclo de vida da verificação
envio do XML ──► Nível 1├─ erro bloqueante ────────────► REJECTED (terminal)└─ aprovado ──► PENDING_SEFAZ ──► VALIDATING (Nível 2)├──► VALIDATED (terminal)├──► REJECTED (terminal, com reason)└──► VALIDATION_ERROR (repetível, com reason)
| validationStatus | Significado |
|---|---|
VALIDATED | A SEFAZ confirma que a nota é autêntica e válida (autorizada, sem cancelamento, protocolo coincide) |
REJECTED | Verificação reprovada: erro bloqueante de nível 1, ou a SEFAZ informa que a nota está cancelada / denegada / inutilizada / não encontrada / com protocolo divergente. reason é informado |
VALIDATION_ERROR | A verificação não pôde ser concluída (limitação da SEFAZ ou falha de consulta após as tentativas). Não é um juízo sobre a nota em si; reenvie mais tarde com forceRevalidate: true |
Pré-requisitos (uma única vez)
- Credencial da aplicação: solicite uma aplicação e receba o
app_secret. Ele é exibido apenas uma vez; guarde-o com segurança. Em caso de vazamento, solicite a rotação. - Assinatura da API: a plataforma habilita sua aplicação para
POST /openapi/v3/consultas/nf-e/xmleGET /openapi/v3/consultas/nf-e/{chave}. - Endpoint de webhook: registre a URL de callback em Registrar webhook e assine o evento
invoice.verify.completed(o único canal de envio dos vereditos de nível 2). Ao salvar a URL, a plataforma envia imediatamente uma entrega de teste comevent_type=webhook.verify; seu endpoint deve responder 2xx para que o salvamento tenha êxito (responder 200 sem processar é suficiente para o evento de teste).
Endpoints
| Endpoint | Finalidade |
|---|---|
POST /openapi/v3/consultas/nf-e/xml | Envia o XML bruto da NF-e; devolve o veredito de nível 1 e a nota interpretada, inicia o nível 2 |
GET /openapi/v3/consultas/nf-e/{chave} | Consulta pela chave de acesso; devolve os dados da nota e, para chaves que você enviou, o bloco validation atual |
Recebendo o veredito final
Quando a verificação de nível 2 na SEFAZ é concluída, a plataforma faz um POST para a URL de webhook registrada. Este é o único canal de envio dos vereditos finais; se uma entrega se perder, a consulta por chave devolve o validationStatus atual e serve como alternativa de polling.
Cabeçalhos da requisição
| Cabeçalho | Descrição |
|---|---|
X-Tffiscal-Event | invoice.verify.completed |
X-Tffiscal-Event-Id | Id do evento, a chave de idempotência: não muda entre tentativas; deduplique por ele |
X-Tffiscal-Delivery-Id | Id da entrega, um por URL de destino; não muda entre as tentativas para esse destino (deduplique pelo id do evento, não por ele) |
X-Tffiscal-Timestamp | Segundos Unix, gerado novamente a cada tentativa |
X-Tffiscal-Signature | hex( HMAC-SHA256( secret, timestamp + "." + body ) ), minúsculas; secret = seu app_secret para uma URL de webhook no nível da aplicação (uma URL de reserva no nível do tenant é assinada com o secret configurado para ela) |
Carga
{"version": "1.0","event_id": "1950000000000001","event_type": "invoice.verify.completed","occurred_at": "2026-07-23T17:16:23Z","data": {"chaveAcesso": "35260764962869000108550990001366171195929648","validationStatus": "VALIDATED","status": "Autorizada","cStat": "100","xMotivo": "Autorizado o uso da NF-e","protocolo": { "numero": "135262955451772", "digestValue": "oAEEuC3tGmb2W7ZJxWkNVWuBHwY=" },"dataAutorizacao": "2026-07-23T14:30:09Z","eventos": [],"verifiedAt": "2026-07-23T17:16:23Z"}}
Campos do envelope:
| Campo | Tipo | Presença | Descrição |
|---|---|---|---|
| version | string | sempre | Versão do schema da carga, atualmente 1.0 |
| event_id | string | sempre | Id do evento, a chave de idempotência; idêntico entre tentativas |
| event_type | string | sempre | invoice.verify.completed |
| occurred_at | string | sempre | Hora do evento, ISO-8601 UTC |
| data | object | sempre | Corpo do veredito, abaixo |
Campos de data:
| Campo | Tipo | Presença | Descrição |
|---|---|---|---|
| chaveAcesso | string | sempre | Chave de acesso de 44 dígitos da nota verificada; chave de junção com o seu envio |
| validationStatus | string | sempre | Veredito final: VALIDATED / REJECTED / VALIDATION_ERROR (veja Ciclo de vida da verificação) |
| status | string | null | anulável | Situação fiscal na SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida; nulo em VALIDATION_ERROR (nenhum veredito da SEFAZ foi obtido) |
| cStat | string | null | anulável | Código de retorno bruto da SEFAZ (ex.: 100 = autorizada, 101 = cancelada); nulo quando a SEFAZ não foi alcançada |
| xMotivo | string | null | anulável | Mensagem de retorno bruta da SEFAZ (português, literal) |
| protocolo | object | null | anulável | Objeto Protocol (numero, digestValue), como na resposta da verificação de XML; do registro oficial |
| dataAutorizacao | string | null | anulável | Hora de autorização na SEFAZ, ISO-8601 UTC |
| eventos[] | array | sempre (pode ser vazio) | Eventos fiscais registrados contra a nota (cancelamento, cartas de correção); vazio quando não há |
| verifiedAt | string | sempre | Quando a verificação de nível 2 foi concluída, ISO-8601 UTC |
| reason | string | somente em falha | Presente somente para REJECTED / VALIDATION_ERROR; texto estável em inglês que explica o veredito |
Nota: as cargas de webhook trazem apenas valores enumerados independentes de idioma; não há campos
*Descriptionaqui. O texto de apresentação fica a cargo do receptor.
Requisitos do receptor
- Verifique a assinatura: recalcule
HMAC-SHA256(secret, timestamp + "." + rawBody)e compare com o cabeçalho. Use os bytes brutos recebidos; não desserialize e serialize de novo antes (a reordenação de campos quebra a assinatura). - Responda 2xx em até 10 segundos. Qualquer outra coisa, incluindo timeouts, conta como entrega falha.
- Entregas falhas são repetidas com backoff 1m / 5m / 30m / 2h / 6h (5 tentativas); reenvio manual disponível sob solicitação.
- Deduplique por
event_id(tentativas e fan-out para múltiplos destinos compartilham o mesmo event_id). - Uma mesma chave pode gerar vários eventos ao longo do tempo: uma nova verificação com
forceRevalidate: true, um reenvio após a janela de 24 horas ou uma rechecagem em segundo plano disparada pela consulta por chave terminam, cada um, em um novo evento com seu próprioevent_id. Fique com o veredito do evento com ooccurred_atmais recente.
Tratamento do veredito
| validationStatus | Terminal? | Ação |
|---|---|---|
VALIDATED | Sim | Seguro prosseguir (liberar mercadoria, liquidar etc.) |
REJECTED | Sim | Não prosseguir; reason explica o veredito da SEFAZ (cancelada / denegada / inutilizada / não encontrada / protocolo divergente) |
VALIDATION_ERROR | Sim, repetível | Falha de verificação do lado da plataforma, não é um juízo sobre a nota. Como qualquer veredito, é reutilizado por 24 horas; reenvie mais tarde com forceRevalidate: true |
Idempotência e nova verificação
- A verificação de XML é idempotente pela chave (a chave de acesso dentro do XML enviado):
- Enquanto o nível 2 está em andamento (
PENDING_SEFAZ/VALIDATING), reenviar devolve o progresso atual e não inicia outra checagem.forceRevalidate: truenão reinicia nem acelera uma checagem já em curso. - Um veredito terminal (
VALIDATED/REJECTED/VALIDATION_ERROR) é reutilizado por 24 horas a partir do momento em que foi obtido: reenviar dentro dessa janela o devolve sem consultar a SEFAZ de novo. Depois de 24 horas, um reenvio simples executa a verificação completa (nível 1 + nível 2) outra vez. - Uma resposta reutilizada reflete o XML enviado originalmente (dados da nota, checagens de nível 1, erros e avisos) junto com o
validationStatusatual. Se você reenviar um XML corrigido para a mesma chave dentro da janela, envieforceRevalidate: true; caso contrário, recebe o resultado anterior.
- Enquanto o nível 2 está em andamento (
- Nova verificação forçada: envie o cabeçalho
forceRevalidate: trueno endpoint de verificação de XML (não faz parte da assinatura). Ele ignora a reutilização e executa o nível 1 e o nível 2 de novo; o novo veredito chega como um novo evento de webhook. - Consulta por chave: não força uma rechecagem. Ela agenda uma por conta própria somente quando a última resposta da SEFAZ tem mais de 24 horas (veja consulta por chave).
- Um
REJECTEDbloqueante de nível 1 não tem registro de nível 2; para verificar de novo, reenvie o XML (comforceRevalidate: truedentro da janela de 24 horas). - As verificações pertencem à sua conta de integrador: a reutilização, a consulta por chave e os webhooks só enxergam os envios da sua conta. Outro integrador verificando a mesma nota não afeta a sua; cada conta é verificada de forma independente e recebe o próprio veredito.
Modelo de erros
Duas formas de erro
Erros de negócio e de requisição (HTTP 400) são devolvidos como objeto simples:
{ "code": 10015004, "message": "Nota fiscal não encontrada: 35260764962869000108550990001366171195929648" }
| Campo | Tipo | Descrição |
|---|---|---|
| code | integer | Código de erro da plataforma |
| message | string | Explicação legível, localizada (veja Localização das respostas) |
Erros da camada de autenticação (HTTP 401 / 403 / 429) são produzidos pelo gateway da plataforma antes de a requisição chegar à API e usam o envelope da plataforma:
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
| Campo | Tipo | Descrição |
|---|---|---|
| success | boolean | false em todo erro |
| errorType | integer | Classe do erro: 1 erro de API, 2 rejeição da SEFAZ, 3 falha de sistema, 4 falha de validação de campos |
| code | integer | Código de erro da plataforma |
| message | string | Explicação legível, localizada |
| data | object | null | Não preenchido em erros |
Seu tratamento de erros precisa aceitar as duas formas. Uma regra robusta: interprete o corpo como JSON, leia code de qualquer uma das formas e trate success=false ou HTTP >= 400 como falha.
Erros de autenticação e autorização
| HTTP | code | Significado | Ação |
|---|---|---|---|
| 401 | 10009000 | Cabeçalhos de assinatura ausentes (token / sign / timestamp) | Corrija o cliente: envie os três cabeçalhos em toda requisição |
| 401 | 10009001 | Timestamp inválido ou desvio de relógio acima de ±300 s | Sincronize o relógio (NTP); gere o timestamp a cada requisição, nunca reutilize |
| 401 | 10009002 | Token inválido | Confira o app_secret; se houve rotação, atualize a configuração |
| 401 | 10009003 | Assinatura divergente | Recalcule a assinatura; veja a lista em Solução de problemas |
| 403 | 10009004 | Aplicação desativada | Contate a plataforma |
| 403 | 10009015 | Aplicação não vigente (aguardando aprovação ou rejeitada) | Aguarde a aprovação / contate a plataforma |
| 403 | 10009014 | Conta do integrador desativada | Contate a plataforma |
| 403 | 10009005 | API não assinada | Solicite a assinatura do endpoint chamado |
| 429 | 10009006 | Limite de taxa excedido | Recue e repita; suavize a taxa de chamadas. Os limites são aplicados por aplicação e por grupo de endpoints |
| 413 | 10009027 | Corpo da requisição acima do limite de tamanho do gateway | A verificação de XML aceita no máximo 1 MB; envie uma única NF-e por requisição |
Orientação de repetição: 401 e 403 são erros de configuração; repetir sem corrigir é inútil e pode disparar o limite de taxa. 429 pode ser repetido com backoff exponencial (inicie em 1 s, dobre até 30 s, adicione jitter).
Erros de requisição e de validação
Documentados em cada página de endpoint:
- Verificar XML: erros de requisição 10015000 a 10015003 (HTTP 400, forma simples, a requisição nunca entra na validação) e o catálogo de validação de nível 1 de
XML_MALFORMEDaXML_VERSION_UNSUPPORTED(HTTP 200, dentro devalidation.errors[]). - Consultar por chave: 10015104 (chave malformada) e 10015004 (nota não encontrada), HTTP 400, forma simples.
- Os resultados de nível 2 não são erros HTTP; veja Tratamento do veredito.
Referência rápida de status HTTP
| HTTP | Cenário | Forma do corpo |
|---|---|---|
| 200 | Requisição aceita, incluindo validação reprovada (verifique o bloco validation) | Dados simples |
| 400 | A própria requisição é inválida (vazia / grande demais / DTD / codificação / chave malformada / não encontrada) | {code, message} simples |
| 401 | Falha de autenticação (token / sign / timestamp) | Envelope da plataforma |
| 413 | Corpo acima do limite de tamanho do gateway | Envelope da plataforma |
| 403 | Aplicação desativada / não vigente / integrador desativado / não assinada | Envelope da plataforma |
| 429 | Limite de taxa excedido | Envelope da plataforma |
| 5xx | Falha do lado da plataforma | Repita com backoff; se persistir, contate a plataforma com o timestamp e o caminho da requisição que falhou |
Localização das respostas
Os campos message e *Description seguem o idioma da requisição: o cabeçalho Language (en / pt / es / zh) tem precedência, depois Accept-Language (aceita pt-BR e valores q). Sem cabeçalho de idioma, as respostas usam português (pt) por padrão.
Para decisões programáticas use sempre os campos enumerados independentes de idioma (validationStatus, status, errors[].code); nunca compare textos descritivos.
Checklist de integração
- Obtenha o
app_secret; confira sua implementação de assinatura com a saída do assistente de assinatura (uma vez para POST com corpo XML, uma vez para GET com corpo vazio). POST /openapi/v3/consultas/nf-e/xmlcom um nfeProc autorizado genuíno: HTTP 200, as cinco verificações de validaçãotrue,validationStatus=PENDING_SEFAZ.- Receba o webhook
invoice.verify.completed: assinatura confere, deduplicado porevent_id, vereditoVALIDATED. - Consulta por chave: uma chave já enviada devolve 200 com os dados da nota e o bloco
validation(mesmovalidationStatusdo veredito do webhook); uma chave inexistente devolve 400 + 10015004; uma chave com dígito verificador errado devolve 400 + 10015104. - Casos negativos: envie um XML adulterado (
SIGNATURE_INVALID); envie um XML sem protNFe (apenas aviso, ainda aceito). - Idempotência: reenvie a mesma chave e receba o veredito reutilizado; envie com
forceRevalidate: truee receba uma nova verificação com novo veredito via webhook. - Caminhos de falha: chame com
signerrado (401, código 10009003); chame um endpoint não assinado (403, código 10009005).
Solução de problemas
A assinatura nunca confere (401, código 10009003)?
Verifique, por ordem de frequência:
- CR/LF não removidos do corpo antes da concatenação;
- requisição GET concatenou
"null"em vez da string vazia como corpo; pathsem o prefixo/openapi, ou incluindo a query string;signenviado em maiúsculas (deve ser hex minúsculo);- o valor de
timestampusado na concatenação difere do cabeçalho (gerado de novo entre os dois); - bytes do corpo recodificados (é preciso fazer o hash exatamente dos bytes enviados na rede, UTF-8).
HTTP 200, mas a nota é falsa?
O nível 1 só avalia se o XML é internamente coerente. A autenticidade é decidida pelo nível 2 na SEFAZ e entregue por webhook; condicione sua ação de negócio (liberação, liquidação) ao VALIDATED do webhook, nunca apenas à resposta síncrona.
O status da resposta síncrona é a situação na SEFAZ?
Não. Na primeira resposta, o status é lido do protocolo dentro do XML que você enviou; a SEFAZ ainda não foi consultada. A situação confirmada pela SEFAZ chega com o veredito de nível 2: o status do webhook, ou a consulta por chave depois que o nível 2 obtém resposta.
REJECTED com status NaoEncontrada: a nota é falsa?
Não necessariamente. A SEFAZ respondeu "não encontrada" (cStat 217). A plataforma não conclui com o primeiro 217: consulta a SEFAZ de novo várias vezes ao longo de alguns minutos, porque uma nota recém-autorizada pode levar um tempo para ficar consultável, e só um 217 persistente vira REJECTED com NaoEncontrada. Causas comuns:
- a nota foi autorizada há instantes e a SEFAZ ainda não terminou de propagá-la; verifique de novo mais tarde com
forceRevalidate: true; - a nota foi emitida no ambiente de homologação da SEFAZ (
ambienteEmissao=Homologacao): o nível 2 consulta o ambiente de produção, onde notas de teste não existem; - a nota nunca foi autorizada com essa chave.
Isso é diferente do 10015004 da consulta por chave, que significa que nenhuma fonte de dados da plataforma conhece a chave.
A assinatura do webhook continua falhando?
A causa mais comum é desserializar a carga e serializá-la de novo antes de calcular o HMAC, o que altera a ordem dos campos ou os espaços em branco. Sempre faça o hash dos bytes brutos recebidos.
VALIDATION_ERROR é um problema da nota?
Não. Significa que o canal de verificação da plataforma falhou (ex.: limitação da SEFAZ); a nota em si não foi julgada. Reenvie mais tarde com forceRevalidate: true.
Desvio de relógio (401, código 10009001)?
O relógio do seu servidor difere do nosso em mais de 300 segundos. Use NTP. Nunca armazene nem reutilize timestamps entre requisições.
