Verificación de NF-e
Verificación en dos niveles de NF-e de terceros: verificación de XML, consulta por chave, veredicto final entregado por webhook, idempotencia y modelo de errores.
Visión general
La plataforma realiza una verificación en dos niveles de NF-e de terceros (modelo 55):
| Nivel | Qué hace | Cuándo |
|---|---|---|
| Nivel 1 (síncrono) | Validación local del XML: sintaxis, layout XSD de la NF-e 4.00, firma digital y titularidad del certificado, estructura de la clave de acceso (chave) de 44 dígitos y coherencia con los campos del documento, integridad del protocolo de autorización | Se devuelve de inmediato en la respuesta de la API |
| Nivel 2 (asíncrono) | Comprobación de autenticidad en tiempo real contra la SEFAZ (la autoridad fiscal estatal): estado de autorización, eventos de cancelación, número de protocolo y digest comparados con los registros oficiales | Se ejecuta tras aprobar el nivel 1; el veredicto se entrega por webhook (vea Recepción del veredicto final), y la consulta por chave devuelve el estado actual como alternativa de sondeo |
Una consulta por chave independiente devuelve los datos de la factura por clave de acceso. Para una chave que usted envió por la verificación de XML, también devuelve el bloque validation actual y sirve como alternativa de sondeo cuando se pierde una entrega del webhook.
Ambos endpoints requieren las tres cabeceras de firma, vea Autenticación. Las respuestas son objetos simples (sin sobre de la plataforma); los errores de solicitud son un objeto simple {code, message}.
Ciclo de vida de la verificación
envío del XML ──► Nivel 1├─ error bloqueante ─────────────► REJECTED (terminal)└─ aprobado ──► PENDING_SEFAZ ──► VALIDATING (Nivel 2)├──► VALIDATED (terminal)├──► REJECTED (terminal, con reason)└──► VALIDATION_ERROR (reintentable, con reason)
| validationStatus | Significado |
|---|---|
VALIDATED | La SEFAZ confirma que la factura es auténtica y válida (autorizada, sin cancelación, protocolo coincide) |
REJECTED | Verificación fallida: error bloqueante de nivel 1, o la SEFAZ informa de que la factura está cancelada / denegada / inutilizada / no encontrada / con protocolo divergente. Se informa reason |
VALIDATION_ERROR | La verificación no pudo completarse (limitación de la SEFAZ o fallo de consulta tras los reintentos). No es un juicio sobre la factura en sí; reenvíe más tarde con forceRevalidate: true |
Requisitos previos (una sola vez)
- Credencial de la aplicación: solicite una aplicación y reciba el
app_secret. Se muestra una sola vez; guárdelo de forma segura. Si se filtra, solicite la rotación. - Suscripción a la API: la plataforma habilita su aplicación para
POST /openapi/v3/consultas/nf-e/xmlyGET /openapi/v3/consultas/nf-e/{chave}. - Endpoint de webhook: registre la URL de callback en Registrar webhook y suscríbase al evento
invoice.verify.completed(el único canal de envío de los veredictos de nivel 2). Al guardar la URL, la plataforma envía de inmediato una entrega de prueba conevent_type=webhook.verify; su endpoint debe responder 2xx para que el guardado tenga éxito (responder 200 sin procesar basta para el evento de prueba).
Endpoints
| Endpoint | Finalidad |
|---|---|
POST /openapi/v3/consultas/nf-e/xml | Envía el XML sin procesar de la NF-e; devuelve el veredicto de nivel 1 y la factura interpretada, inicia el nivel 2 |
GET /openapi/v3/consultas/nf-e/{chave} | Consulta por clave de acceso; devuelve los datos de la factura y, para las chaves que usted envió, el bloque validation actual |
Recepción del veredicto final
Cuando la verificación de nivel 2 en la SEFAZ concluye, la plataforma hace un POST a la URL de webhook registrada. Este es el único canal de envío de los veredictos finales; si se pierde una entrega, la consulta por chave devuelve el validationStatus actual y sirve como alternativa de sondeo.
Cabeceras de la solicitud
| Cabecera | Descripción |
|---|---|
X-Tffiscal-Event | invoice.verify.completed |
X-Tffiscal-Event-Id | Id del evento, la clave de idempotencia: no cambia entre reintentos; deduplique por ella |
X-Tffiscal-Delivery-Id | Id de la entrega, uno por URL de destino; no cambia entre los reintentos a ese destino (deduplique por el id del evento, no por este) |
X-Tffiscal-Timestamp | Segundos Unix, regenerado en cada intento |
X-Tffiscal-Signature | hex( HMAC-SHA256( secret, timestamp + "." + body ) ), en minúsculas; secret = su app_secret para una URL de webhook a nivel de aplicación (una URL de respaldo a nivel de tenant se firma con el secret configurado para ella) |
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 del sobre:
| Campo | Tipo | Presencia | Descripción |
|---|---|---|---|
| version | string | siempre | Versión del esquema de la carga, actualmente 1.0 |
| event_id | string | siempre | Id del evento, la clave de idempotencia; idéntico entre reintentos |
| event_type | string | siempre | invoice.verify.completed |
| occurred_at | string | siempre | Hora del evento, ISO-8601 UTC |
| data | object | siempre | Cuerpo del veredicto, más abajo |
Campos de data:
| Campo | Tipo | Presencia | Descripción |
|---|---|---|---|
| chaveAcesso | string | siempre | Clave de acceso de 44 dígitos de la factura verificada; clave de unión con su envío |
| validationStatus | string | siempre | Veredicto final: VALIDATED / REJECTED / VALIDATION_ERROR (vea Ciclo de vida de la verificación) |
| status | string | null | anulable | Estado fiscal en la SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida; null en VALIDATION_ERROR (no se obtuvo veredicto de la SEFAZ) |
| cStat | string | null | anulable | Código de retorno bruto de la SEFAZ (p. ej. 100 = autorizada, 101 = cancelada); null cuando no se alcanzó la SEFAZ |
| xMotivo | string | null | anulable | Mensaje de retorno bruto de la SEFAZ (portugués, literal) |
| protocolo | object | null | anulable | Objeto Protocol (numero, digestValue), como en la respuesta de la verificación de XML; del registro oficial |
| dataAutorizacao | string | null | anulable | Hora de autorización en la SEFAZ, ISO-8601 UTC |
| eventos[] | array | siempre (puede estar vacío) | Eventos fiscales registrados contra la factura (cancelación, cartas de corrección); vacío cuando no hay |
| verifiedAt | string | siempre | Cuándo se completó la verificación de nivel 2, ISO-8601 UTC |
| reason | string | solo en fallo | Presente solo para REJECTED / VALIDATION_ERROR; texto estable en inglés que explica el veredicto |
Nota: las cargas de webhook solo llevan valores enumerados independientes del idioma; aquí no hay campos
*Description. El texto de presentación corre a cargo del receptor.
Requisitos del receptor
- Verifique la firma: recalcule
HMAC-SHA256(secret, timestamp + "." + rawBody)y compárelo con la cabecera. Use los bytes recibidos sin procesar; no deserialice y vuelva a serializar antes (reordenar los campos rompe la firma). - Responda 2xx en menos de 10 segundos. Cualquier otra cosa, incluidos los timeouts, cuenta como entrega fallida.
- Las entregas fallidas se reintentan con backoff 1m / 5m / 30m / 2h / 6h (5 intentos); reenvío manual disponible bajo petición.
- Deduplique por
event_id(los reintentos y el fan-out a varios destinos comparten el mismo event_id). - Una misma chave puede generar varios eventos con el tiempo: una nueva verificación con
forceRevalidate: true, un reenvío pasada la ventana de 24 horas o una comprobación en segundo plano disparada por la consulta por chave terminan, cada uno, en un evento nuevo con su propioevent_id. Quédese con el veredicto del evento con eloccurred_atmás reciente.
Tratamiento del veredicto
| validationStatus | ¿Terminal? | Acción |
|---|---|---|
VALIDATED | Sí | Seguro continuar (liberar mercancía, liquidar, etc.) |
REJECTED | Sí | No continuar; reason explica el veredicto de la SEFAZ (cancelada / denegada / inutilizada / no encontrada / protocolo divergente) |
VALIDATION_ERROR | Sí, reintentable | Fallo de verificación del lado de la plataforma, no es un juicio sobre la factura. Como cualquier veredicto, se reutiliza durante 24 horas; reenvíe más tarde con forceRevalidate: true |
Idempotencia y nueva verificación
- La verificación de XML es idempotente por la chave (la clave de acceso dentro del XML enviado):
- Mientras el nivel 2 está en curso (
PENDING_SEFAZ/VALIDATING), reenviar devuelve el progreso actual y no inicia otra comprobación.forceRevalidate: trueno reinicia ni acelera una comprobación que ya está en marcha. - Un veredicto terminal (
VALIDATED/REJECTED/VALIDATION_ERROR) se reutiliza durante 24 horas desde el momento en que se obtuvo: reenviar dentro de esa ventana lo devuelve sin volver a consultar a la SEFAZ. Pasadas 24 horas, un reenvío normal ejecuta de nuevo la verificación completa (nivel 1 + nivel 2). - Una respuesta reutilizada refleja el XML enviado originalmente (datos de la factura, comprobaciones de nivel 1, errores y avisos) junto con el
validationStatusactual. Si reenvía un XML corregido para la misma chave dentro de la ventana, envíeforceRevalidate: true; de lo contrario recibe el resultado anterior.
- Mientras el nivel 2 está en curso (
- Nueva verificación forzada: envíe la cabecera
forceRevalidate: trueen el endpoint de verificación de XML (no forma parte de la firma). Omite la reutilización y ejecuta de nuevo el nivel 1 y el nivel 2; el nuevo veredicto llega como un nuevo evento de webhook. - Consulta por chave: no puede forzar una comprobación. Programa una por su cuenta solo cuando la última respuesta de la SEFAZ tiene más de 24 horas (vea consulta por chave).
- Un
REJECTEDbloqueante de nivel 1 no tiene registro de nivel 2; para verificar de nuevo, reenvíe el XML (conforceRevalidate: truedentro de la ventana de 24 horas). - Las verificaciones pertenecen a su cuenta de integrador: la reutilización, la consulta por chave y los webhooks solo ven los envíos de su cuenta. Que otro integrador verifique la misma factura no afecta a la suya; cada cuenta se verifica de forma independiente y recibe su propio veredicto.
Modelo de errores
Dos formas de error
Los errores de negocio y de solicitud (HTTP 400) se devuelven como objeto simple:
{ "code": 10015004, "message": "Factura no encontrada: 35260764962869000108550990001366171195929648" }
| Campo | Tipo | Descripción |
|---|---|---|
| code | integer | Código de error de la plataforma |
| message | string | Explicación legible, localizada (vea Localización de las respuestas) |
Los errores de la capa de autenticación (HTTP 401 / 403 / 429) los produce el gateway de la plataforma antes de que la solicitud llegue a la API, y usan el sobre de la plataforma:
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
| Campo | Tipo | Descripción |
|---|---|---|
| success | boolean | false en todo error |
| errorType | integer | Clase del error: 1 error de API, 2 rechazo de la SEFAZ, 3 fallo del sistema, 4 fallo de validación de campos |
| code | integer | Código de error de la plataforma |
| message | string | Explicación legible, localizada |
| data | object | null | No se rellena en los errores |
Su gestor de errores debe aceptar ambas formas. Una regla robusta: interprete el cuerpo como JSON, lea code de cualquiera de las formas y trate success=false o HTTP >= 400 como fallo.
Errores de autenticación y autorización
| HTTP | code | Significado | Acción |
|---|---|---|---|
| 401 | 10009000 | Faltan cabeceras de firma (token / sign / timestamp) | Corrija el cliente: envíe las tres cabeceras en cada solicitud |
| 401 | 10009001 | Timestamp inválido o desfase de reloj superior a ±300 s | Sincronice el reloj (NTP); genere el timestamp en cada solicitud, nunca lo reutilice |
| 401 | 10009002 | Token inválido | Compruebe el app_secret; si se rotó, actualice la configuración |
| 401 | 10009003 | Firma no coincide | Recalcule la firma; vea la lista en Solución de problemas |
| 403 | 10009004 | Aplicación desactivada | Contacte con la plataforma |
| 403 | 10009015 | Aplicación no vigente (pendiente de aprobación o rechazada) | Espere la aprobación / contacte con la plataforma |
| 403 | 10009014 | Cuenta del integrador desactivada | Contacte con la plataforma |
| 403 | 10009005 | API no suscrita | Solicite la suscripción al endpoint llamado |
| 429 | 10009006 | Límite de tasa superado | Retroceda y reintente; suavice la tasa de llamadas. Los límites se aplican por aplicación y por grupo de endpoints |
| 413 | 10009027 | El cuerpo de la solicitud supera el límite de tamaño del gateway | La verificación de XML acepta como máximo 1 MB; envíe una sola NF-e por solicitud |
Guía de reintentos: 401 y 403 son errores de configuración; reintentar sin corregir es inútil y puede disparar el límite de tasa. 429 es reintentable con backoff exponencial (empiece en 1 s, duplique hasta 30 s, añada jitter).
Errores de solicitud y de validación
Documentados en cada página de endpoint:
- Verificación de XML: errores de solicitud 10015000 a 10015003 (HTTP 400, forma simple, la solicitud nunca entra en la validación) y el catálogo de validación de nivel 1 de
XML_MALFORMEDaXML_VERSION_UNSUPPORTED(HTTP 200, dentro devalidation.errors[]). - Consulta por chave: 10015104 (chave malformada) y 10015004 (factura no encontrada), HTTP 400, forma simple.
- Los resultados de nivel 2 no son errores HTTP; vea Tratamiento del veredicto.
Referencia rápida de estados HTTP
| HTTP | Escenario | Forma del cuerpo |
|---|---|---|
| 200 | Solicitud aceptada, incluida la validación fallida (compruebe el bloque validation) | Datos simples |
| 400 | La propia solicitud es inválida (vacía / demasiado grande / DTD / codificación / chave malformada / no encontrada) | {code, message} simple |
| 401 | Fallo de autenticación (token / sign / timestamp) | Sobre de la plataforma |
| 413 | Cuerpo por encima del límite de tamaño del gateway | Sobre de la plataforma |
| 403 | Aplicación desactivada / no vigente / integrador desactivado / no suscrita | Sobre de la plataforma |
| 429 | Límite de tasa superado | Sobre de la plataforma |
| 5xx | Fallo del lado de la plataforma | Reintente con backoff; si persiste, contacte con la plataforma con el timestamp y la ruta de la solicitud fallida |
Localización de las respuestas
Los campos message y *Description siguen el idioma de la solicitud: la cabecera Language (en / pt / es / zh) tiene prioridad, después Accept-Language (admite pt-BR y valores q). Sin cabecera de idioma, las respuestas usan portugués (pt) por defecto.
Para las decisiones programáticas use siempre los campos enumerados independientes del idioma (validationStatus, status, errors[].code); nunca compare textos descriptivos.
Lista de comprobación de integración
- Obtenga el
app_secret; contraste su implementación de firma con la salida del asistente de firma (una vez para POST con cuerpo XML, una vez para GET con cuerpo vacío). POST /openapi/v3/consultas/nf-e/xmlcon un nfeProc autorizado genuino: HTTP 200, las cinco comprobaciones de validación entrue,validationStatus=PENDING_SEFAZ.- Reciba el webhook
invoice.verify.completed: la firma se verifica, deduplicado porevent_id, veredictoVALIDATED. - Consulta por chave: una chave enviada previamente devuelve 200 con los datos de la factura y el bloque
validation(el mismovalidationStatusque el veredicto del webhook); una chave inexistente devuelve 400 + 10015004; una chave con dígito verificador incorrecto devuelve 400 + 10015104. - Casos negativos: envíe un XML alterado (
SIGNATURE_INVALID); envíe un XML sin protNFe (solo aviso, se sigue aceptando). - Idempotencia: reenvíe la misma chave y reciba el veredicto reutilizado; envíe con
forceRevalidate: truey reciba una nueva verificación con nuevo veredicto vía webhook. - Rutas de fallo: llame con un
signincorrecto (401, código 10009003); llame a un endpoint no suscrito (403, código 10009005).
Solución de problemas
¿La firma nunca coincide (401, código 10009003)?
Compruebe, por orden de frecuencia:
- CR/LF no eliminados del cuerpo antes de la concatenación;
- una solicitud GET concatenó
"null"en lugar de la cadena vacía como cuerpo; pathsin el prefijo/openapi, o incluyendo la query string;signenviado en mayúsculas (debe ser hex en minúsculas);- el valor de
timestampusado en la concatenación difiere del de la cabecera (regenerado entre ambos); - bytes del cuerpo recodificados (hay que hacer el hash exactamente de los bytes enviados por la red, UTF-8).
¿HTTP 200 pero la factura es falsa?
El nivel 1 solo juzga si el XML es internamente coherente. La autenticidad la decide el nivel 2 contra la SEFAZ y se entrega por webhook; condicione su acción de negocio (liberación, liquidación) al VALIDATED del webhook, nunca solo a la respuesta síncrona.
¿El status de la respuesta síncrona es el estado en la SEFAZ?
No. En la primera respuesta, el status se lee del protocolo dentro del XML que usted envió; todavía no se ha consultado a la SEFAZ. El estado confirmado por la SEFAZ llega con el veredicto de nivel 2: el status del webhook, o la consulta por chave una vez que el nivel 2 obtiene respuesta.
REJECTED con status NaoEncontrada: ¿la factura es falsa?
No necesariamente. La SEFAZ respondió "no encontrada" (cStat 217). La plataforma no concluye con el primer 217: vuelve a consultar a la SEFAZ varias veces a lo largo de unos minutos, porque una factura recién autorizada puede tardar en ser consultable, y solo un 217 persistente se convierte en REJECTED con NaoEncontrada. Causas habituales:
- la factura se autorizó hace instantes y la SEFAZ aún no terminó de propagarla; verifique de nuevo más tarde con
forceRevalidate: true; - la factura se emitió en el ambiente de homologación de la SEFAZ (
ambienteEmissao=Homologacao): el nivel 2 consulta el ambiente de producción, donde las facturas de prueba no existen; - la factura nunca se autorizó con esa chave.
Esto es distinto del 10015004 de la consulta por chave, que significa que ninguna fuente de datos de la plataforma conoce la chave.
¿La firma del webhook sigue fallando?
La causa más común es deserializar la carga y volver a serializarla antes de calcular el HMAC, lo que cambia el orden de los campos o los espacios en blanco. Haga siempre el hash de los bytes recibidos sin procesar.
¿VALIDATION_ERROR es un problema de la factura?
No. Significa que el canal de verificación de la plataforma falló (p. ej. limitación de la SEFAZ); la factura en sí no fue juzgada. Reenvíe más tarde con forceRevalidate: true.
¿Desfase de reloj (401, código 10009001)?
El reloj de su servidor difiere del nuestro en más de 300 segundos. Use NTP. Nunca guarde en caché ni reutilice timestamps entre solicitudes.
