Webhooks
Catálogo de eventos, cabeceras de entrega, firma HMAC-SHA256, payloads por tipo de documento, reintentos y circuit breaker de los webhooks de TF Fiscal.
Modelo de entrega
La plataforma envía los resultados de los documentos y los veredictos de verificación a su servidor, sin necesidad de polling.
- Una URL de callback por aplicación, registrada en Registrar webhook. Llamar de nuevo al endpoint sobrescribe la URL y el
token. Los resultados de NF-e, CT-e y DC-e y el veredicto de la verificación de NF-e se entregan todos en esa URL. - Entrega al menos una vez (at-least-once). Un reintento tras un timeout puede llegar aunque el intento original ya se haya procesado, y el mismo evento puede distribuirse a varios destinos. Deduplique por el id del evento (cabecera
X-Tffiscal-Event-Idy, en los payloads con sobre,event_id): es idéntico en todos los intentos del mismo evento. - Cada entrega es un HTTP
POSTcon cuerpo JSON (Content-Type: application/json; charset=utf-8) a la URL registrada. - Los documentos de prueba y de producción siguen el mismo contrato, la misma firma y el mismo calendario de reintentos, vea Entornos.
Catálogo de eventos
| Evento | Se dispara cuando | Payload |
|---|---|---|
invoice.authorized | NF-e autorizada por SEFAZ (cStat 100) | Objeto de resultado NF-e, nfeStatus = Autorizada |
invoice.rejected | NF-e rechazada por SEFAZ | Objeto de resultado NF-e, nfeStatus = Negada, motivo en nfeMotivoStatus |
invoice.canceled | Cancelación de la NF-e registrada en SEFAZ (cStat 135) | Sobre de evento, data trae invoice_id / chave / protocolo |
invoice.cce.registered | Carta de corrección (CC-e) registrada en una NF-e | Sobre de evento, data trae invoice_id / chave / n_seq. Reservado: el registro de la CC-e es síncrono hoy y este evento todavía no se entrega |
invoice.verify.completed | La verificación de NF-e llegó al veredicto final (VALIDATED / REJECTED / VALIDATION_ERROR) | Sobre de evento con el veredicto en data |
cte.authorized | CT-e autorizado (cStat 100) | Objeto de resultado CT-e, cteStatus = Autorizada |
cte.rejected | CT-e rechazado por SEFAZ (cStat distinto de 100) | Objeto de resultado CT-e, cteStatus = Negada, cteMotivoStatus = cStat - motivo |
cte.canceled | Cancelación del CT-e registrada (cStat 135) | Objeto de resultado CT-e, cteStatus = Cancelada |
cte.event.registered | Cualquier otro evento de CT-e registrado (carta de corrección, comprobante de entrega, entrega fallida, desacuerdo de servicio y sus cancelaciones) | Sobre de evento, data trae event_code / n_seq / protocolo |
dce.authorized | DC-e autorizada (cStat 100) | Objeto de resultado DC-e, dceStatus = Autorizada |
dce.rejected | DC-e rechazada por SEFAZ, o la tarea de emisión falló de forma terminal (incluso antes de numerar el documento) | Objeto de resultado DC-e, dceStatus = Negada |
dce.canceled | Cancelación de la DC-e registrada (cStat 135 / 136 / 155) | Objeto de resultado DC-e, dceStatus = Cancelada |
dce.cancel_rejected | SEFAZ rechazó la cancelación de la DC-e, o la tarea de cancelación falló de forma terminal | Objeto de resultado DC-e, dceStatus = CancelamentoNegado, el documento sigue autorizado |
Se usan dos formas de payload:
- Objeto de resultado del documento: objeto JSON plano cuyo primer campo es
tipo(NF-e/CT-e/DC-e), con orden fijo de campos. Lo usan los resultados de autorización, rechazo y cancelación de cada tipo de documento. No tiene campoevent_id; deduplique por la cabeceraX-Tffiscal-Event-Id. - Sobre de evento:
{ "version", "event_id", "event_type", "occurred_at", "data" }. Lo usaninvoice.verify.completedy las notificaciones de registro de eventos. Los campos dentro dedatasolo se añaden, nunca se renombran ni se eliminan; un cambio incompatible incrementaversion.
Nota: los rechazos de SEFAZ nunca son errores HTTP de la llamada de emisión. Aparecen como estado
Negadaen la consulta y como eventoinvoice.rejected/cte.rejected/dce.rejected. Una tarea de CT-e que falla de forma terminal del lado de la plataforma (Falha) no genera callback; use la consulta de CT-e para detectarla.
Cabeceras de la petición
| Cabecera | Significado |
|---|---|
X-Tffiscal-Event | Código del evento, por ejemplo invoice.authorized (webhook.verify en la entrega de prueba enviada al guardar una URL en la consola) |
X-Tffiscal-Event-Id | Id del evento, la clave de idempotencia; no cambia entre reintentos y es compartido por todos los destinos del mismo evento |
X-Tffiscal-Delivery-Id | Identificador de la entrega; no deduplique por él, use el id del evento |
X-Tffiscal-Timestamp | Segundos Unix, regenerado en cada intento |
X-Tffiscal-Signature | hex( HMAC-SHA256( app_secret, timestamp + "." + body ) ), en minúsculas |
token | El token de verificación que envió en Registrar webhook, devuelto sin cambios |
x-token | Mismo valor que token; verifique cualquiera de los dos |
Verificar la firma
Calcule HMAC-SHA256 sobre la cadena timestamp + "." + rawBody con su app_secret y compárelo en tiempo constante con la cabecera X-Tffiscal-Signature. Use los bytes crudos recibidos: deserializar y volver a serializar el payload cambia el orden de los campos o los espacios y rompe la firma. Rechace entregas con timestamp demasiado antiguo (5 minutos es una tolerancia razonable) para evitar replay.
Se recomienda verificar la firma. Como mínimo, compare la cabecera token con el valor registrado.
Node.js:
const crypto = require('node:crypto');/*** Verifica una entrega de webhook de TF Fiscal.* @param {string} secret su app_secret* @param {string} timestamp valor de la cabecera X-Tffiscal-Timestamp* @param {Buffer|string} rawBody cuerpo crudo de la petición, sin parsear* @param {string} signature valor de la cabecera X-Tffiscal-Signature* @returns {boolean} true cuando la firma es auténtica*/function verifyWebhook(secret, timestamp, rawBody, signature) {const expected = crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody, 'utf8').digest('hex');return (expected.length === signature.length &&crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signature, 'utf8')));}
Java:
import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;public final class WebhookVerifier {/*** Verifica una entrega de webhook de TF Fiscal.** @param secret su app_secret* @param timestamp valor de la cabecera X-Tffiscal-Timestamp* @param rawBody cuerpo crudo de la petición, sin parsear* @param signature valor de la cabecera X-Tffiscal-Signature* @return true cuando la firma es auténtica*/public static boolean verify(String secret, String timestamp, String rawBody, String signature) {try {Mac mac = Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));byte[] digest = mac.doFinal((timestamp + "." + rawBody).getBytes(StandardCharsets.UTF_8));StringBuilder hex = new StringBuilder(digest.length * 2);for (byte b : digest) {hex.append(String.format("%02x", b));}return MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8),signature.getBytes(StandardCharsets.UTF_8));} catch (Exception e) {return false;}}}
Payload de NF-e
Eventos invoice.authorized e invoice.rejected. Se registran en Registrar webhook; vea NF-e para el flujo de emisión.
Autorizada:
{"tipo": "NF-e","empresaId": "1934811222334455","nfeId": "NFe-000014553","nfeStatus": "Autorizada","nfeLinkDanfe": "https://api.v2.tffiscal.com/openapi/files/danfe/35241204893402000113650010000117691017244265?token=MXw3fDEwMXwxNzU4...","nfeLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/8801?token=MXw3fDEwMXwxNzU4...","nfeNumero": "11769","nfeSerie": "10","nfeChaveAcesso": "35241204893402000113650010000117691017244265","nfeDataEmissao": "2024-12-04T17:44:26Z","nfeDataAutorizacao": "2024-12-04T17:44:26Z","nfeNumeroProtocolo": "135240002599237"}
Denegada:
{"tipo": "NF-e","empresaId": "1934811222334455","nfeId": "NFe-000014553","nfeStatus": "Negada","nfeMotivoStatus": "778 - Rejeicao: NCM inexistente","nfeNumero": "11769","nfeSerie": "10","nfeChaveAcesso": "35241204893402000113650010000117691017244265","nfeDataEmissao": "2024-12-04T17:44:26Z"}
| Campo | Tipo | Descripción |
|---|---|---|
tipo | string | Siempre NF-e |
empresaId | string | Identificador de la empresa |
nfeId | string | El id enviado en la emisión |
nfeStatus | string | Autorizada / Negada |
nfeMotivoStatus | string | Motivo de la denegación: código de estado de SEFAZ + descripción; vacío cuando está autorizada |
nfeLinkDanfe | string | Enlace de descarga del PDF del DANFE (vea Descarga de archivos; utilizable en el momento del callback, la primera descarga dispara el renderizado); vacío cuando está denegada |
nfeLinkXml | string | Enlace de descarga del XML autorizado; vacío cuando está denegada |
nfeNumero | string | Número de la factura |
nfeSerie | string | Serie |
nfeChaveAcesso | string | Clave de acceso de 44 dígitos |
nfeDataEmissao | string | Fecha de emisión, ISO-8601 UTC |
nfeDataAutorizacao | string | Fecha de autorización; vacío cuando está denegada |
nfeNumeroProtocolo | string | Número de protocolo de autorización; vacío cuando está denegada |
nfeDigestValue | string | Digest de la firma, no se proporciona por ahora |
Los campos sin valor aparecen en el payload como valores vacíos.
Cancelación y carta de corrección de NF-e
invoice.canceled usa el sobre de evento con data trayendo invoice_id (identificador de la factura en la plataforma), chave (clave de acceso de 44 dígitos) y protocolo (protocolo del evento de cancelación). La cancelación en sí se confirma de forma síncrona en Cancelar NF-e y por el estado Cancelada en la consulta.
invoice.cce.registered está reservado en el catálogo (data: invoice_id / chave / n_seq). El endpoint de registro de la CC-e devuelve el protocolo de forma síncrona y por ahora no se entrega ningún callback para él.
Payload de CT-e
Mismo registro y mismas cabeceras que la NF-e; vea CT-e para el flujo de emisión. El objeto de resultado usa tipo = CT-e con orden fijo de campos.
| Evento | Disparo | cteStatus |
|---|---|---|
cte.authorized | Autorización 100 | Autorizada |
cte.rejected | Rechazo de SEFAZ (cStat distinto de 100) | Negada (cteMotivoStatus es cStat - motivo); un fallo terminal de la tarea (Falha) no genera callback, use la API de consulta |
cte.canceled | Cancelación 135 | Cancelada |
cte.event.registered | Cualquier otro evento registrado | Sobre de evento, data trae event_code / n_seq / protocolo |
Autorizado:
{"tipo": "CT-e","empresaId": "1934811222334455","cteId": "CTE-ORD-1","cteStatus": "Autorizada","cteMotivoStatus": null,"cteLinkDacte": "https://api.v2.tffiscal.com/openapi/files/dacte/3526...?token=...","cteLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","cteNumero": "1","cteSerie": "1","cteChaveAcesso": "3526...","cteDataEmissao": "2026-09-06T12:00:00Z","cteDataAutorizacao": "2026-09-06T12:00:03Z","cteNumeroProtocolo": "135260000000001","cteDigestValue": "..."}
| Campo | Tipo | Descripción |
|---|---|---|
tipo | string | Siempre CT-e |
empresaId | string | Identificador de la empresa |
cteId | string | El id enviado en la emisión |
cteStatus | string | Autorizada / Negada / Cancelada |
cteMotivoStatus | string | null | cStat - motivo cuando está rechazado; null en los demás casos |
cteLinkDacte | string | null | Enlace de descarga del PDF del DACTE (renderizado en la primera descarga); null cuando está rechazado |
cteLinkXml | string | null | Enlace de descarga del XML autorizado; null cuando está rechazado |
cteNumero | string | Número del documento |
cteSerie | string | Serie |
cteChaveAcesso | string | Clave de acceso de 44 dígitos |
cteDataEmissao | string | Fecha de emisión, ISO-8601 UTC |
cteDataAutorizacao | string | null | Fecha de autorización; null cuando está rechazado |
cteNumeroProtocolo | string | null | Número de protocolo de autorización; null cuando está rechazado |
cteDigestValue | string | null | Digest de la firma del XML autorizado |
cteLinkDacte y cteLinkXml son utilizables en cuanto llega el callback de autorización; los enlaces no necesitan cabeceras de firma y redirigen con 302, vea Descarga de archivos.
Evento registrado (cte.event.registered), por ejemplo un comprobante de entrega (110180):
{"version": "1.0","event_id": "987654321098765432","event_type": "cte.event.registered","occurred_at": "2026-09-06T13:00:00Z","data": {"event_code": "110180","n_seq": "1","protocolo": "135260000000099"}}
| Campo | Tipo | Descripción |
|---|---|---|
event_code | string | Código del evento en SEFAZ, por ejemplo 110110 carta de corrección, 110180 comprobante de entrega |
n_seq | string | Número de secuencia del evento |
protocolo | string | Número de protocolo del evento |
Payload de DC-e
Mismo registro y mismas cabeceras que la NF-e; vea DC-e para el flujo de emisión. El objeto de resultado usa tipo = DC-e con 14 campos en orden fijo.
| Evento | Disparo | dceStatus |
|---|---|---|
dce.authorized | Autorización 100 | Autorizada |
dce.rejected | Rechazo de SEFAZ o fallo terminal de la tarea (incluidos los fallos antes de numerar el documento, que no tienen chave) | Negada (dceMotivoStatus es cStat - motivo; un fallo terminal sin cStat trae solo el motivo) |
dce.canceled | Cancelación registrada (135 / 136 / 155) | Cancelada (dceDataAutorizacao es el momento de registro de la cancelación, dceNumeroProtocolo el protocolo del evento de cancelación) |
dce.cancel_rejected | SEFAZ rechazó la cancelación o la tarea de cancelación falló de forma terminal | CancelamentoNegado (el documento sigue autorizado; trae el protocolo de autorización / digest / enlace del XML) |
Autorizada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Autorizada","dceMotivoStatus": null,"dceLinkDace": "https://api.v2.tffiscal.com/openapi/files/dace/35260940673061000134990010000000011100000012?token=...","dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "41260940673061000134990010000000011101234567","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": "2026-09-06T12:00:03Z","dceNumeroProtocolo": "141260000000001","dceDigestValue": "tasG40Ic6Zh7PIBiLEEH7Hb940Y="}
Rechazada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Negada","dceMotivoStatus": "225 - Rejeicao: Falha no schema XML","dceLinkDace": null,"dceLinkXml": null,"dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": null,"dceDigestValue": null}
Fallo antes de la numeración (empresa no configurada para DC-e, serie ausente, fallo de mapeo del mensaje y otros fallos terminales en los que el documento nunca fue numerado y no tiene chave): dce.rejected se entrega igualmente, los campos de hecho del documento son null, dceMotivoStatus trae el motivo del fallo y dceDataEmissao el momento de aceptación. Correlacione por dceId y nunca suponga que dceChaveAcesso está presente:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Negada","dceMotivoStatus": "Empresa não configurada para emissão de DC-e","dceLinkDace": null,"dceLinkXml": null,"dceNumero": null,"dceSerie": null,"dceChaveAcesso": null,"dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": null,"dceDigestValue": null}
Cancelada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "Cancelada","dceMotivoStatus": null,"dceLinkDace": "https://api.v2.tffiscal.com/openapi/files/dace/35260940673061000134990010000000011100000012?token=...","dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": "2026-09-06T15:00:00Z","dceNumeroProtocolo": "141260000000099","dceDigestValue": null}
Cancelación rechazada:
{"tipo": "DC-e","empresaId": "1934811222334455","dceId": "DCe-000012333","dceStatus": "CancelamentoNegado","dceMotivoStatus": "594 - Rejeicao: O numero de sequencia do evento informado e maior que o permitido","dceLinkDace": null,"dceLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/7?token=...","dceNumero": "1","dceSerie": "1","dceChaveAcesso": "4126...","dceDataEmissao": "2026-09-06T12:00:00Z","dceDataAutorizacao": null,"dceNumeroProtocolo": "141260000000001","dceDigestValue": "tasG40Ic6Zh7PIBiLEEH7Hb940Y="}
| Campo | Tipo | Descripción |
|---|---|---|
tipo | string | Siempre DC-e |
empresaId | string | Identificador de la empresa |
dceId | string | El id enviado en la emisión; clave de correlación de todos los eventos, presente incluso cuando el documento nunca fue numerado |
dceStatus | string | Autorizada / Negada / Cancelada / CancelamentoNegado |
dceMotivoStatus | string | null | cStat - motivo en un rechazo de SEFAZ; solo el motivo en un fallo terminal sin cStat; null en éxito |
dceLinkDace | string | null | Enlace de descarga del PDF del DACE, renderizado en la primera descarga; null cuando está rechazada y en CancelamentoNegado |
dceLinkXml | string | null | Enlace de descarga del XML autorizado; null cuando está rechazada |
dceNumero | string | null | Número del documento; null cuando nunca fue numerado |
dceSerie | string | null | Serie; null cuando nunca fue numerado |
dceChaveAcesso | string | null | Clave de acceso de 44 dígitos; null cuando nunca fue numerado |
dceDataEmissao | string | Fecha de emisión, ISO-8601 UTC (momento de aceptación en un fallo antes de la numeración) |
dceDataAutorizacao | string | null | Fecha de autorización; momento de registro de la cancelación en Cancelada; null cuando está rechazada |
dceNumeroProtocolo | string | null | Protocolo de autorización; protocolo del evento de cancelación en Cancelada; null cuando está rechazada |
dceDigestValue | string | null | Digest de la firma del XML autorizado; null cuando está rechazada y en Cancelada |
En los callbacks de autorización / cancelación, dceLinkDace se renderiza bajo demanda por chave (nada se renderiza en el momento de la entrega; la primera descarga renderiza y archiva). Ambos enlaces comparten la forma de la API de consulta (/openapi/files/{kind}/{ref}?token=...) y su vigencia viene de la configuración del tenant (7 días por defecto), vea Descarga de archivos.
Payload del veredicto de verificación
Cuando la verificación de nivel 2 en SEFAZ se resuelve, la plataforma envía invoice.verify.completed a su URL de webhook. Este es el único canal push para veredictos finales; la consulta por chave puede servir como respaldo por polling.
{"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": "oAEE...HwY=" },"dataAutorizacao": "2026-07-23T14:30:09Z","eventos": [],"verifiedAt": "2026-07-23T17:16:23Z","reason": "present only for REJECTED / VALIDATION_ERROR (stable English text)"}}
Sobre:
| Campo | Tipo | Presencia | Descripción |
|---|---|---|---|
version | string | siempre | Versión del esquema del payload, actualmente 1.0 |
event_id | string | siempre | Id del evento, clave de idempotencia, idéntico entre reintentos |
event_type | string | siempre | invoice.verify.completed |
occurred_at | string | siempre | Momento del evento, ISO-8601 UTC |
data | object | siempre | Cuerpo del veredicto, abajo |
data:
| Campo | Tipo | Presencia | Descripción |
|---|---|---|---|
chaveAcesso | string | siempre | Clave de acceso de 44 dígitos de la factura verificada, la clave de unión con su envío |
validationStatus | string | siempre | Veredicto final: VALIDATED / REJECTED / VALIDATION_ERROR, vea Verificación de NF-e |
status | string | siempre | Estado fiscal en SEFAZ: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
cStat | string | null | anulable | Código de retorno crudo de SEFAZ (por ejemplo 100 autorizada, 101 cancelada); null cuando no se alcanzó SEFAZ |
xMotivo | string | null | anulable | Mensaje de retorno crudo de SEFAZ (portugués, sin cambios) |
protocolo | object | null | anulable | Objeto de protocolo (numero, digestValue) del registro oficial |
dataAutorizacao | string | null | anulable | Fecha de autorización en SEFAZ, ISO-8601 UTC |
eventos[] | array | siempre (puede estar vacío) | Eventos fiscales registrados sobre 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 en REJECTED / VALIDATION_ERROR; texto estable en inglés que explica el veredicto |
validationStatus | Terminal | Acción |
|---|---|---|
VALIDATED | Sí | Seguro continuar (liberar mercancía, liquidar) |
REJECTED | Sí | No continúe; reason explica el veredicto de 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 por la verificación de XML con la cabecera forceRevalidate: true |
Los payloads de webhook solo traen valores de enumeración independientes del idioma; no hay campos *Description. El texto de presentación queda a cargo del receptor.
Reintentos y circuit breaker
- Cualquier estado 2xx confirma la entrega. Cualquier otro estado, un error de red o la falta de respuesta en 10 segundos cuenta como fallo.
- Las entregas fallidas se reintentan con backoff tras el intento inicial:
1 min, 5 min, 30 min, 2 h, 6 h
- Cinco reintentos (seis intentos en total, en unas 8,6 horas). Después, la entrega queda en la cola de dead-letter; la plataforma puede reenviarla manualmente a petición. Cada intento se registra con su estado HTTP, duración y resumen de la respuesta, que es la evidencia usada al investigar un callback ausente.
- Circuit breaker: los fallos de entrega consecutivos (10 por defecto) desactivan automáticamente el webhook. Mientras está desactivado no se encola ningún evento nuevo para él. Llamar de nuevo a Registrar webhook (o revalidar y guardar la URL en la consola) restaura la entrega y pone a cero el contador de fallos; pida a la plataforma el reenvío de los eventos generados mientras el webhook estaba desactivado.
- La URL del webhook y el
tokense releen antes de cada intento, por lo que un nuevo registro surte efecto en el siguiente reintento.
Requisitos del receptor
- Responda 2xx en menos de 10 segundos. Buena práctica: persista la entrega cruda, responda 2xx de inmediato y procese de forma asíncrona.
- Verifique el origen: recalcule el HMAC sobre los bytes crudos recibidos y compare la cabecera
tokencon el valor registrado. - Deduplique por el id del evento (
X-Tffiscal-Event-Idoevent_iden los payloads con sobre). - URL pública: una URL absoluta
http/httpsaccesible desde internet, de como máximo 500 caracteres. Las direcciones de loopback, privadas y link-local se rechazan en el registro. - Responda a la entrega de prueba: guardar la URL en la consola envía un evento con
X-Tffiscal-Event: webhook.verify; responda 2xx sin ningún procesamiento de negocio. - Condicione las acciones de negocio a los campos de resultado (
nfeStatus,cteStatus,dceStatus,validationStatus), nunca solo a la respuesta síncrona de la API. - Nunca suponga que los campos opcionales están presentes:
dceChaveAcessopuede sernull,reasonsolo aparece en fallo y los campos vacíos de NF-e llegan como valores vacíos.
Solución de problemas
¿No llegan los callbacks?
Confirme que la URL es una dirección https/http accesible públicamente y que responde 2xx en menos de 10 segundos. La plataforma reintenta con backoff y desactiva el webhook tras fallos consecutivos; llamar de nuevo al endpoint de registro restaura la entrega.
¿La firma falla siempre?
La causa más común es deserializar el payload y volver a serializarlo antes de calcular el HMAC, lo que cambia el orden de los campos o los espacios. Calcule siempre el hash sobre los bytes crudos recibidos y concatene el valor de X-Tffiscal-Timestamp exactamente como lo recibió.
¿El mismo evento llegó dos veces?
Es lo esperado con la entrega al menos una vez. Deduplique por el id del evento; el id de entrega varía por destino y no debe usarse para eso.
