Webhook para Fintechs

Implementa la validación técnica y de seguridad de mensajes recibidos por webhook: autenticidad, integridad, antireplay, idempotencia y trazabilidad.

Voultech puede entregar eventos a tu integración vía Webhook o vía Azure Service Bus — ambos canales entregan los mismos eventos. Esta guía describe cómo implementar el receptor webhook y validar correctamente cada mensaje para garantizar autenticidad, integridad, antireplay, idempotencia y trazabilidad.

💡

Si prefieres consumir eventos en modo pull (sin exponer un endpoint público), consulta la guía de Azure Service Bus.

Objetivos de seguridad

ObjetivoDescripción
AutenticidadConfirmar que el emisor del webhook es Voultech.
IntegridadConfirmar que el payload no fue modificado en tránsito.
AntireplayEvitar reenvíos maliciosos del mismo webhook.
IdempotenciaEvitar efectos duplicados ante reintentos (normales en webhooks).
TrazabilidadAuditar qué se recibió, cuándo y cómo se procesó.

Requisitos de transporte

  • HTTPS obligatorio (TLS 1.2+).
  • Método: POST
  • Content-Type: application/json
  • Timeouts: responde rápido (ideal < 2–5 s) y procesa asincrónicamente si el negocio lo requiere.
  • Respuesta: 2xx solo si fue aceptado y validado. Usa 4xx para errores del request y 5xx para fallas internas.

Cabeceras del mensaje

Cada webhook enviado por Voultech incluye las siguientes cabeceras:

CabeceraDescripción
X-TimestampEpoch en segundos (UTC). Usado para antireplay y como parte de la firma.
X-Idempotency-KeyClave idempotente del envío.
X-SignatureFirma HMAC-SHA256 del payload, con el prefijo sha256=.

Ejemplo:

X-Timestamp: 1730000000
X-Idempotency-Key: 8b5c1a2e-...
X-Signature: sha256=BASE64_HMAC_SHA256

Validación paso a paso

1. Validación básica del request

  • Verificar que el método es POST.
  • Verificar el Content-Type esperado (application/json).
  • Parsear el JSON y validar el esquema mínimo.

2. Validar autenticidad e integridad (firma)

Se usa HMAC-SHA256 con secreto compartido. Voultech calcula la firma sobre el body crudo y la codifica en Base64. Tu integración debe recalcular y comparar en tiempo constante.

Construcción de la firma (exacto):

signing_string = X-Timestamp + "." + raw_body
signature      = base64(hmac_sha256(secret, signing_string))

Formato esperado del header X-Signature:

X-Signature: sha256=<signature_base64>
⚠️

La firma debe calcularse sobre el body crudo (raw body), nunca sobre el JSON reserializado. La comparación debe realizarse siempre en tiempo constante para evitar ataques de timing.

3. Validación antireplay (timestamp + ventana)

  • Verificar que X-Timestamp esté dentro de la ventana tolerable.
  • La tolerancia configurada es WebhookSignature:MaxSkewSeconds (por defecto 300 segundos).
  • Si el timestamp está fuera de la ventana: rechazar con 401.

4. Validación de idempotencia (X-Idempotency-Key)

  • Extraer X-Idempotency-Key.
  • Persistirlo con una restricción UNIQUE en tu base de datos.
  • Si el evento ya existe: responder 200 / 2xx y no reprocesar (o reprocesar solo bajo reglas claras si quedó en estado failed).

Códigos HTTP de respuesta

CasoCódigo
Firma inválida / timestamp inválido401 o 403
JSON inválido / esquema inválido400
Evento duplicado (idempotencia)200 (recomendado para no incentivar reintentos)
Falla interna temporal500 (fuerza reintento por parte de Voultech)

Ejemplo de implementación en C#

using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

static string ComputeHmacSha256Base64(string secret, string data)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(data)));
}

static bool ConstantTimeEqualsBase64(string a, string b)
{
    try
    {
        var ba = Convert.FromBase64String(a);
        var bb = Convert.FromBase64String(b);
        return CryptographicOperations.FixedTimeEquals(ba, bb);
    }
    catch (FormatException)
    {
        return false;
    }
}

app.MapPost("/webhook", async ([FromServices] IConfiguration config, HttpRequest request) =>
{
    var timestampHeader  = request.Headers["X-Timestamp"].ToString();
    var signatureHeader  = request.Headers["X-Signature"].ToString();

    // 1) Validación de headers obligatorios
    if (string.IsNullOrWhiteSpace(timestampHeader) || string.IsNullOrWhiteSpace(signatureHeader))
        return Results.Unauthorized();

    // 2) Parse de timestamp
    if (!long.TryParse(timestampHeader, out var ts))
        return Results.Unauthorized();

    // 3) Validación antireplay (skew)
    var maxSkewSeconds = config.GetValue<int?>("WebhookSignature:MaxSkewSeconds") ?? 300;
    var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (Math.Abs(now - ts) > maxSkewSeconds)
        return Results.Unauthorized();

    // 4) Obtener secret
    var secret = config["WebhookSignature:Secret"];
    if (string.IsNullOrWhiteSpace(secret))
        return Results.Problem("Missing WebhookSignature:Secret", statusCode: 500);

    // 5) Normalizar firma (remover prefijo sha256= si viene)
    var receivedSignature = signatureHeader.StartsWith("sha256=", StringComparison.OrdinalIgnoreCase)
        ? signatureHeader["sha256=".Length..]
        : signatureHeader;

    // 6) Leer raw body y recalcular firma
    using var reader = new StreamReader(request.Body);
    var rawBody = await reader.ReadToEndAsync();

    var signingString       = $"{timestampHeader}.{rawBody}";
    var expectedSignature   = ComputeHmacSha256Base64(secret, signingString);

    // 7) Comparación en tiempo constante
    if (!ConstantTimeEqualsBase64(expectedSignature, receivedSignature))
        return Results.Unauthorized();

    // OK: el webhook es auténtico e íntegro
    return Results.Ok(new { status = "validated" });
});

Ejemplo de mensaje JSON

El mensaje JSON enviado por Voultech sigue la misma estructura de eventos del sistema. Ejemplo para el topic KYCVALIDATION:

{
  "Topic": "KYCVALIDATION",
  "Date": "2026-01-21 18:59:00",
  "Code": "TEST_FINTECH",
  "Fintech": "TEST_FINTECH",
  "Detail": {
    "Information": "Transaction validated successfully",
    "UserId": "12345678",
    "State": "APPROVED",
    "documentType": "DNI",
    "documentNumber": "12345678",
    "riskScore": 12,
    "provider": "Sumsub",
    "country": "CL"
  }
}
💡

El campo Topic identifica el tipo de evento. Los topics disponibles son los mismos que se describen en la sección Sistema de Eventos (MOVBANCO, REVERSAMOVBANCO, MOVSHINKANSEN, KYCVALIDATION).


Did this page help you?