Requisitos en Masnet
Antes de configurar el sistema receptor, prepare el destino en Masnet. Los pasos son los mismos para todos los sistemas.
- Abra Empresa › Integraciones, vaya a la pestaña Destinos y seleccione Nuevo destino. Si la integración aún no tiene datos autorizados, Masnet le pedirá autorizarlos primero.
- En Dirección del destino (https) escriba la URL pública del receptor. Debe ser
https, puerto 443 u 8443 y un nombre DNS público: no se admiten usuario en la URL, fragmento, direcciones IP ni nombres internos. Se admite una cadena de consulta; Masnet trata la URL completa como una credencial. - En Formato elija JSON estándar (
application/json) o CloudEvents estructurado (application/cloudevents+json). El contenido del evento es el mismo; consulte Webhooks. - Seleccione Crear destino. Masnet muestra el secreto de firma (
whsec_…) una sola vez. Guárdelo de inmediato en el almacén de secretos del receptor; no podrá consultarlo después. - Seleccione Probar destino y luego Enviar prueba. La integración solo se activa con una prueba exitosa; después use Activar destino.
Datos que necesita el verificador
- El secreto de firma (
whsec_…) del destino, guardado fuera del código. - El cuerpo exacto recibido, sin interpretar. En ASP.NET Core lea
Request.Bodycon unStreamReaderantes de cualquier enlace de modelos; no deserialice primero. - Las cabeceras
webhook-id,webhook-timestampywebhook-signature. - Una tolerancia de reloj de 5 minutos, que es la que aplican las bibliotecas de Standard Webhooks y la recomendada para el receptor.
La firma es v1, seguido de base64(HMAC-SHA256) calculado con la clave binaria que resulta de decodificar en base64 lo que sigue a whsec_, sobre los bytes webhook-id + . + webhook-timestamp + . + cuerpo exacto. Verifique siempre sobre los bytes recibidos, antes de interpretar el JSON.
Con la biblioteca StandardWebhooks
Instálela con dotnet add package StandardWebhooks --version 1.1.2. Es un paquete de la comunidad que usa Microsoft.AspNetCore.Http, de modo que el proyecto debe ser ASP.NET Core; la clase del paquete es StandardWebhook.
// dotnet add package StandardWebhooks --version 1.1.2 (requires the ASP.NET Core shared framework)
using Microsoft.AspNetCore.Http;
using StandardWebhooks;
using StandardWebhooks.Diagnostics;
public static class CSharpStandardWebhooks
{
/// <summary>
/// payload is the unparsed request body; Request.Headers carries the webhook-* headers; the library enforces a 5 minute tolerance.
/// The library only checks the first signature of the header, so each one is verified on its own (during a rotation the header carries two).
/// </summary>
public static bool Verify(string secret, string payload, IHeaderDictionary headers)
{
var webhook = new StandardWebhook(secret);
foreach (var signature in headers["webhook-signature"].ToString().Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
var single = new HeaderDictionary
{
["webhook-id"] = headers["webhook-id"],
["webhook-timestamp"] = headers["webhook-timestamp"],
["webhook-signature"] = signature,
};
try
{
webhook.Verify(payload, single);
return true;
}
catch (WebhookVerificationException)
{
// try the next signature
}
}
return false;
}
}Sin dependencias
La clase compara en tiempo constante y acepta cualquiera de las firmas del encabezado.
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
/// <summary>Verifies a Masnet webhook with the base class library only. rawBody is the exact bytes received.</summary>
public static class CSharpManual
{
public static bool Verify(string secret, IDictionary<string, string> headers, byte[] rawBody, long nowSeconds, long tolerance = 300)
{
if (!headers.TryGetValue("webhook-id", out var id) || !headers.TryGetValue("webhook-timestamp", out var timestamp)
|| !headers.TryGetValue("webhook-signature", out var signatures) || !long.TryParse(timestamp, out var sent)) return false;
if (Math.Abs(nowSeconds - sent) > tolerance) return false;
var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret.Substring(6) : secret);
using var hmac = new HMACSHA256(key);
var prefix = Encoding.UTF8.GetBytes($"{id}.{timestamp}.");
var signed = new byte[prefix.Length + rawBody.Length];
prefix.CopyTo(signed, 0);
rawBody.CopyTo(signed, prefix.Length);
var expected = hmac.ComputeHash(signed);
// During a rotation the header carries two signatures separated by a space; any match is valid.
foreach (var part in signatures.Split(' '))
{
var pair = part.Split(',', 2);
if (pair.Length != 2 || pair[0] != "v1") continue;
try
{
if (CryptographicOperations.FixedTimeEquals(Convert.FromBase64String(pair[1]), expected)) return true;
}
catch (FormatException) { /* ignore a malformed entry */ }
}
return false;
}
}Operación: duplicados y orden
- Idempotencia.
webhook-ididentifica el evento y se conserva en los reintentos y los reenvíos. Registre los identificadores ya procesados y descarte los repetidos con una respuesta 2xx. - Orden. Cada recurso (
subject) lleva unsequencecreciente. Descarte un evento cuyosequencesea menor o igual al último que aplicó para esesubject. Los eventos de recursos distintos no tienen orden entre sí. - Reintentos. Masnet reintenta cualquier respuesta distinta de 2xx durante un máximo de 72 horas; el calendario completo está en Webhooks.
- Rotación del secreto. Al rotar con superposición, el secreto anterior sigue vigente 24 horas y
webhook-signaturetrae dos firmasv1,…separadas por un espacio. Acepte el evento si cualquiera coincide.
Diagnóstico
Masnet registra el código de respuesta de cada intento en el historial de entregas. Estos son los efectos de cada respuesta del receptor:
| Respuesta del receptor | Efecto en Masnet |
|---|---|
| 2xx | Entrega exitosa. Significa recepción, no procesamiento de negocio. |
| 3xx | No se sigue la redirección. Cuenta como fallo y se reintenta; el historial muestra el Location sin su cadena de consulta. |
| 400, 401, 403, 404 y otros 4xx o 5xx | Fallo reintentable hasta 72 horas desde el inicio del ciclo de entrega. |
| 410 | Masnet desactiva el destino y cancela lo pendiente. Use 410 solo si el destino debe dejar de recibir eventos. |
| 413 | El evento se marca como no entregable a ese destino y no se reintenta. |
429 o 503 con Retry-After | Se respeta Retry-After (segundos o fecha HTTP) hasta un máximo de 1 hora. |
| Sin respuesta en 15 segundos | Tiempo agotado (5 s de conexión, 15 s en total). Es un fallo reintentable: responda 2xx y procese de forma asíncrona. |
| La verificación falla solo en producción | Casi siempre el cuerpo fue leído tras el enlace de modelos. Lea los bytes originales. |