Los webhooks permiten a tu sistema recibir notificaciones en tiempo real cuando ocurren eventos dentro de NUFI, evitando la necesidad de consultar continuamente la API.
Cuando ocurre un evento, NUFI enviará una solicitud POST a la URL que configures junto con la información del evento.
Configuración
Para utilizar webhooks, debes:
Definir una URL pública que pueda recibir solicitudes POST
Configurar un secret para validar la autenticidad del webhook
Seleccionar al menos un evento
Debes seleccionar al menos un evento para registrar un webhook.
Eventos disponibles
status_changed Se dispara cuando cambia el estado general de un expediente (Ej: Pendiente, En Revisión, Completado).
document_submitted Notifica cuando un documento es enviado o actualizado.
document_verdict Se dispara cuando un documento recibe un veredicto (Aprobado/Rechazado).
channel_changed Se activa cuando cambia el canal de comunicación o método de verificación.
section_feedback Notifica cuando se actualiza una sección del expediente con retroalimentación.
document_json_export Envía la información completa del expediente en formato JSON al finalizar el proceso.
Estructura del Request
{
"content-type" : "application/json" ,
"x-nufi-event" : "status_changed" ,
"x-nufi-signature" : "v1=firma_hmac_sha256" ,
"x-nufi-webhook-timestamp" : "timestamp_unix" ,
"x-nufi-webhook-id" : "uuid_del_webhook"
}
Body (Ejemplo)
Se anexan ejemplos para cada tipo de evento
status_changed
{
"Type" : "status_changed" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"Timestamp" : "2026-04-07T23:31:09.104111Z" ,
"User" : "4a5d3f24c0a247b88d7432ca25a2f51b" ,
"NewStatus" : "Pendiente de revisión por analista" ,
"NextResponsible" : "Analista" ,
"ExternalId" : ""
}
document_submitted
{
"Type" : "document_submitted" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"ExternalId" : null ,
"Timestamp" : "2026-04-07T23:31:09.7698766Z" ,
"PreviousStatus" : "Ingresado" ,
"NewStatus" : "Recibido"
}
document_verdict
{
"Type" : "document_verdict" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"ExternalId" : null ,
"Timestamp" : "2026-04-07T23:31:18.5780469Z" ,
"Approved" : true ,
"Comments" : "Expediente APROBADO: Se encontró un documento de identificación oficial (Pasaporte) para la persona registrada. No es necesario presentar INE o Licencia si ya existe un Pasaporte válido en el expediente."
}
channel_changed
{
"Type" : "channel_changed" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"Timestamp" : "2026-04-07T23:32:03.1556061Z" ,
"User" : "Sistema" ,
"PreviousChannel" : "Sin canal" ,
"NewChannel" : "Validacion" ,
"ExternalId" : ""
}
section_feedback
{
"Type" : "section_feedback" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"Timestamp" : "2026-04-07T23:32:49.0068553Z" ,
"User" : "soporte@nufi.mx" ,
"Section" : "ConstanciaFiscal" ,
"Status" : "Rechazado" ,
"Comment" : "Documento en B/N" ,
"ExternalId" : ""
}
document_json_export
{
"Type" : "document_json_export" ,
"DocumentId" : "27a2e1b1-d3f0-47f3-bf69-fb339c5fd861" ,
"ExternalId" : null ,
"Timestamp" : "2026-04-07T23:33:13.1582278Z" ,
"Data" : { ... }
}
Validación de firma
Para garantizar que el webhook proviene de NUFI, debes validar la firma utilizando tu secret. Antes de usar el HMACSHA256 deberás hacer un slice para quitar la versión del valor del signature, por defecto viene como v1= y seguido del hash hmac
Generación de la firma:
signedPayload = timestamp + "." + rawBody
signature = HMACSHA256(secret, signedPayload)
Donde:
timestamp: Header x-nufi-webhook-timestamp
rawBody: Body sin modificar (raw)
secret: Tu clave privada
JavaScript (Node.js)
const crypto = require ( "crypto" );
function verifyWebhookSignature ({ rawBody , signature , timestamp , secret }) {
const signedPayload = ` ${ timestamp } . ${ rawBody } ` ;
const expectedSignature = crypto
. createHmac ( "sha256" , secret )
. update ( signedPayload , "utf8" )
. digest ( "hex" );
// Remover el prefijo "v1=" si existe
const cleanSignature = signature . startsWith ( "v1=" )
? signature . slice ( 3 )
: signature ;
return crypto . timingSafeEqual (
Buffer . from ( cleanSignature , "hex" ),
Buffer . from ( expectedSignature , "hex" )
);
}
// Express
app . post ( "/webhook" , ( req , res ) => {
const signature = req . headers [ "x-nufi-signature" ];
const timestamp = req . headers [ "x-nufi-webhook-timestamp" ];
const rawBody = req . rawBody ; // IMPORTANTE: usar body crudo
const secret = process . env . NUFI_WEBHOOK_SECRET ;
const isValid = verifyWebhookSignature ({
rawBody ,
signature ,
timestamp ,
secret
});
if ( ! isValid ) {
return res . status ( 401 ). send ( "Invalid signature" );
}
const event = JSON . parse ( rawBody );
console . log ( "Evento recibido:" , event );
res . status ( 200 ). send ( "OK" );
});
Python (Flask)
import hmac
import hashlib
from flask import Flask, request, abort
from secrets import compare_digest
app = Flask( __name__ )
def verify_webhook_signature ( raw_body : bytes , signature : str , timestamp : str , secret : str ) -> bool :
signed_payload = f " { timestamp } . { raw_body.decode( 'utf-8' ) } "
expected_signature = hmac.new(
key = secret.encode( "utf-8" ),
msg = signed_payload.encode( "utf-8" ),
digestmod = hashlib.sha256
).hexdigest()
# Remover el prefijo "v1=" si existe
clean_signature = signature[ 3 :] if signature.startswith( "v1=" ) else signature
return compare_digest(clean_signature, expected_signature)
@app.post ( "/webhook" )
def webhook ():
signature = request.headers.get( "x-nufi-signature" , "" )
timestamp = request.headers.get( "x-nufi-webhook-timestamp" , "" )
# Importante: obtener el body crudo sin modificar
raw_body = request.get_data( cache = False , as_text = False )
secret = os.environ.get( "NUFI_WEBHOOK_SECRET" , "" )
if not verify_webhook_signature(raw_body, signature, timestamp, secret):
abort( 401 , "Invalid signature" )
event = request.get_json( force = True , silent = False )
print ( "Evento recibido:" , event)
return ( "OK" , 200 )
C# (.NET / ASP.NET Core)
using System . Security . Cryptography ;
using System . Text ;
using Microsoft . AspNetCore . Mvc ;
[ ApiController ]
public class WebhookController : ControllerBase
{
private static bool VerifyWebhookSignature ( string rawBody , string signature , string timestamp , string secret )
{
var signedPayload = $" { timestamp } . { rawBody } " ;
using var hmac = new HMACSHA256 ( Encoding . UTF8 . GetBytes ( secret ));
var hash = hmac . ComputeHash ( Encoding . UTF8 . GetBytes ( signedPayload ));
var expectedSignature = Convert . ToHexString ( hash ). ToLowerInvariant ();
// Remover el prefijo "v1=" si existe
var cleanSignature = signature . StartsWith ( "v1=" )
? signature . Substring ( 3 )
: signature ;
return CryptographicOperations . FixedTimeEquals (
Encoding . UTF8 . GetBytes ( cleanSignature ),
Encoding . UTF8 . GetBytes ( expectedSignature )
);
}
[ HttpPost ]
[ Route ( "webhook" )]
public IActionResult Post ()
{
var signature = Request . Headers [ "x-nufi-signature" ]. ToString ();
var timestamp = Request . Headers [ "x-nufi-webhook-timestamp" ]. ToString ();
using var reader = new StreamReader ( Request . Body , Encoding . UTF8 );
var rawBody = reader . ReadToEnd ();
var secret = Environment . GetEnvironmentVariable ( "NUFI_WEBHOOK_SECRET" ) ?? "" ;
if ( ! VerifyWebhookSignature ( rawBody , signature , timestamp , secret ))
{
return Unauthorized ( "Invalid signature" );
}
// Parsear evento
// var evt = JsonSerializer.Deserialize<JsonElement>(rawBody);
Console . WriteLine ( $"Evento recibido: { rawBody } " );
return Ok ( "OK" );
}
}
PHP (Laravel/Plain PHP)
<? php
function verify_webhook_signature ( string $rawBody , string $signature , string $timestamp , string $secret ) : bool {
$signedPayload = $timestamp . "." . $rawBody ;
$expected = hash_hmac ( 'sha256' , $signedPayload , $secret );
// Remover el prefijo "v1=" si existe
$cleanSignature = str_starts_with ( $signature , 'v1=' )
? substr ( $signature , 3 )
: $signature ;
return hash_equals ( $cleanSignature , $expected );
}
// Plain PHP
$signature = $_SERVER [ 'HTTP_X_NUFI_SIGNATURE' ] ?? '' ;
$timestamp = $_SERVER [ 'HTTP_X_NUFI_WEBHOOK_TIMESTAMP' ] ?? '' ;
$rawBody = file_get_contents ( 'php://input' ); // Body crudo
$secret = getenv ( 'NUFI_WEBHOOK_SECRET' ) ?: '' ;
if ( ! verify_webhook_signature ( $rawBody , $signature , $timestamp , $secret )) {
http_response_code ( 401 );
echo "Invalid signature" ;
exit ;
}
$event = json_decode ( $rawBody , true );
error_log ( "Evento recibido: " . print_r ( $event , true ));
http_response_code ( 200 );
echo "OK" ;