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) |
Una consulta por chave independiente devuelve los datos de la factura por clave de acceso. Es un endpoint de consulta pura y no trae veredicto de verificación.
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 |
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 pura por clave de acceso; devuelve los datos de la factura y no trae veredicto de verificación |
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; la consulta por chave puede servir 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, único por intento |
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 |
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 | siempre | Estado fiscal en la SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
| 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) y después se aparcan en una cola de mensajes muertos (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).
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 | No | Fallo de verificación del lado de la plataforma, no es un juicio sobre la factura; reenvíe más tarde con forceRevalidate: true |
Idempotencia y nueva verificación
- La verificación de XML es idempotente por la chave: reenviar mientras una verificación está en curso devuelve el progreso actual; los veredictos terminales se reutilizan durante 24 horas (sin coste de verificación duplicado).
- Nueva verificación forzada: envíe la cabecera
forceRevalidate: trueen el endpoint de verificación de XML (no forma parte de la firma). La consulta por chave tiene forma GET fija y no dispone de canal de nueva verificación; para forzar una nueva comprobación, reenvíe por el endpoint de XML. - Un
REJECTEDbloqueante de nivel 1 no tiene registro de nivel 2; la nueva verificación exige reenviar el XML.
Modelo de errores
Dos formas de error
Los errores de negocio y de solicitud (HTTP 400) se devuelven como objeto simple:
{ "code": 10015004, "message": "Invoice not found" }
| 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 |
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 |
| 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; 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.
¿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.
