Skip to main content
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:
  1. Definir una URL pública que pueda recibir solicitudes POST
  2. Configurar un secret para validar la autenticidad del webhook
  3. 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

Headers

{
  "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";