TF Fiscal
Documentación

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):

NivelQué haceCuá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ónSe 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 oficialesSe 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

text
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)
validationStatusSignificado
VALIDATEDLa SEFAZ confirma que la factura es auténtica y válida (autorizada, sin cancelación, protocolo coincide)
REJECTEDVerificació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_ERRORLa 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)

  1. 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.
  2. Suscripción a la API: la plataforma habilita su aplicación para POST /openapi/v3/consultas/nf-e/xml y GET /openapi/v3/consultas/nf-e/{chave}.
  3. 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 con event_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

EndpointFinalidad
POST /openapi/v3/consultas/nf-e/xmlEnví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

CabeceraDescripción
X-Tffiscal-Eventinvoice.verify.completed
X-Tffiscal-Event-IdId del evento, la clave de idempotencia: no cambia entre reintentos; deduplique por ella
X-Tffiscal-Delivery-IdId 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-TimestampSegundos Unix, regenerado en cada intento
X-Tffiscal-Signaturehex( 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

json
{
"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:

CampoTipoPresenciaDescripción
versionstringsiempreVersión del esquema de la carga, actualmente 1.0
event_idstringsiempreId del evento, la clave de idempotencia; idéntico entre reintentos
event_typestringsiempreinvoice.verify.completed
occurred_atstringsiempreHora del evento, ISO-8601 UTC
dataobjectsiempreCuerpo del veredicto, más abajo

Campos de data:

CampoTipoPresenciaDescripción
chaveAcessostringsiempreClave de acceso de 44 dígitos de la factura verificada; clave de unión con su envío
validationStatusstringsiempreVeredicto final: VALIDATED / REJECTED / VALIDATION_ERROR (vea Ciclo de vida de la verificación)
statusstring | nullanulableEstado fiscal en la SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida; null en VALIDATION_ERROR (no se obtuvo veredicto de la SEFAZ)
cStatstring | nullanulableCódigo de retorno bruto de la SEFAZ (p. ej. 100 = autorizada, 101 = cancelada); null cuando no se alcanzó la SEFAZ
xMotivostring | nullanulableMensaje de retorno bruto de la SEFAZ (portugués, literal)
protocoloobject | nullanulableObjeto Protocol (numero, digestValue), como en la respuesta de la verificación de XML; del registro oficial
dataAutorizacaostring | nullanulableHora de autorización en la SEFAZ, ISO-8601 UTC
eventos[]arraysiempre (puede estar vacío)Eventos fiscales registrados contra la factura (cancelación, cartas de corrección); vacío cuando no hay
verifiedAtstringsiempreCuándo se completó la verificación de nivel 2, ISO-8601 UTC
reasonstringsolo en falloPresente 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

  1. 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).
  2. Responda 2xx en menos de 10 segundos. Cualquier otra cosa, incluidos los timeouts, cuenta como entrega fallida.
  3. Las entregas fallidas se reintentan con backoff 1m / 5m / 30m / 2h / 6h (5 intentos); reenvío manual disponible bajo petición.
  4. Deduplique por event_id (los reintentos y el fan-out a varios destinos comparten el mismo event_id).
  5. 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 propio event_id. Quédese con el veredicto del evento con el occurred_at más reciente.

Tratamiento del veredicto

validationStatus¿Terminal?Acción
VALIDATEDSíSeguro continuar (liberar mercancía, liquidar, etc.)
REJECTEDSíNo continuar; reason explica el veredicto de la SEFAZ (cancelada / denegada / inutilizada / no encontrada / protocolo divergente)
VALIDATION_ERRORSí, reintentableFallo 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: true no 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 validationStatus actual. Si reenvía un XML corregido para la misma chave dentro de la ventana, envíe forceRevalidate: true; de lo contrario recibe el resultado anterior.
  • Nueva verificación forzada: envíe la cabecera forceRevalidate: true en 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 REJECTED bloqueante de nivel 1 no tiene registro de nivel 2; para verificar de nuevo, reenvíe el XML (con forceRevalidate: true dentro 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:

json
{ "code": 10015004, "message": "Factura no encontrada: 35260764962869000108550990001366171195929648" }
CampoTipoDescripción
codeintegerCódigo de error de la plataforma
messagestringExplicació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:

json
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
CampoTipoDescripción
successbooleanfalse en todo error
errorTypeintegerClase del error: 1 error de API, 2 rechazo de la SEFAZ, 3 fallo del sistema, 4 fallo de validación de campos
codeintegerCódigo de error de la plataforma
messagestringExplicación legible, localizada
dataobject | nullNo 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

HTTPcodeSignificadoAcción
40110009000Faltan cabeceras de firma (token / sign / timestamp)Corrija el cliente: envíe las tres cabeceras en cada solicitud
40110009001Timestamp inválido o desfase de reloj superior a ±300 sSincronice el reloj (NTP); genere el timestamp en cada solicitud, nunca lo reutilice
40110009002Token inválidoCompruebe el app_secret; si se rotó, actualice la configuración
40110009003Firma no coincideRecalcule la firma; vea la lista en Solución de problemas
40310009004Aplicación desactivadaContacte con la plataforma
40310009015Aplicación no vigente (pendiente de aprobación o rechazada)Espere la aprobación / contacte con la plataforma
40310009014Cuenta del integrador desactivadaContacte con la plataforma
40310009005API no suscritaSolicite la suscripción al endpoint llamado
42910009006Límite de tasa superadoRetroceda y reintente; suavice la tasa de llamadas. Los límites se aplican por aplicación y por grupo de endpoints
41310009027El cuerpo de la solicitud supera el límite de tamaño del gatewayLa 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_MALFORMED a XML_VERSION_UNSUPPORTED (HTTP 200, dentro de validation.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

HTTPEscenarioForma del cuerpo
200Solicitud aceptada, incluida la validación fallida (compruebe el bloque validation)Datos simples
400La propia solicitud es inválida (vacía / demasiado grande / DTD / codificación / chave malformada / no encontrada){code, message} simple
401Fallo de autenticación (token / sign / timestamp)Sobre de la plataforma
413Cuerpo por encima del límite de tamaño del gatewaySobre de la plataforma
403Aplicación desactivada / no vigente / integrador desactivado / no suscritaSobre de la plataforma
429Límite de tasa superadoSobre de la plataforma
5xxFallo del lado de la plataformaReintente 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

  1. 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).
  2. POST /openapi/v3/consultas/nf-e/xml con un nfeProc autorizado genuino: HTTP 200, las cinco comprobaciones de validación en true, validationStatus=PENDING_SEFAZ.
  3. Reciba el webhook invoice.verify.completed: la firma se verifica, deduplicado por event_id, veredicto VALIDATED.
  4. Consulta por chave: una chave enviada previamente devuelve 200 con los datos de la factura y el bloque validation (el mismo validationStatus que el veredicto del webhook); una chave inexistente devuelve 400 + 10015004; una chave con dígito verificador incorrecto devuelve 400 + 10015104.
  5. Casos negativos: envíe un XML alterado (SIGNATURE_INVALID); envíe un XML sin protNFe (solo aviso, se sigue aceptando).
  6. Idempotencia: reenvíe la misma chave y reciba el veredicto reutilizado; envíe con forceRevalidate: true y reciba una nueva verificación con nuevo veredicto vía webhook.
  7. Rutas de fallo: llame con un sign incorrecto (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:

  1. CR/LF no eliminados del cuerpo antes de la concatenación;
  2. una solicitud GET concatenó "null" en lugar de la cadena vacía como cuerpo;
  3. path sin el prefijo /openapi, o incluyendo la query string;
  4. sign enviado en mayúsculas (debe ser hex en minúsculas);
  5. el valor de timestamp usado en la concatenación difiere del de la cabecera (regenerado entre ambos);
  6. 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:

  1. la factura se autorizó hace instantes y la SEFAZ aún no terminó de propagarla; verifique de nuevo más tarde con forceRevalidate: true;
  2. 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;
  3. 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.