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)

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

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

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

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, único por intento
X-Tffiscal-TimestampSegundos Unix, regenerado en cada intento
X-Tffiscal-Signaturehex( HMAC-SHA256( secret, timestamp + "." + body ) ), en minúsculas; secret = su app_secret

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)
statusstringsiempreEstado fiscal en la SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
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) y después se aparcan en una cola de mensajes muertos (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).

Tratamiento del veredicto

validationStatus¿Terminal?Acción
VALIDATEDSeguro continuar (liberar mercancía, liquidar, etc.)
REJECTEDNo continuar; reason explica el veredicto de la SEFAZ (cancelada / denegada / inutilizada / no encontrada / protocolo divergente)
VALIDATION_ERRORNoFallo 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: true en 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 REJECTED bloqueante 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:

json
{ "code": 10015004, "message": "Invoice not found" }
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

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
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; 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.

¿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.