API v1
Webhooks
Lo que te avisamos sin que preguntes, para que no tengas que sondear el padrón. El evento que los justifica no es la comodidad: es la baja.
Si una persona revoca su consentimiento con nosotros y tú no te enteras, sigue en tu
base sin querer estarlo. Eso no es un retraso: es un incumplimiento, tuyo y nuestro.
Si vas a escuchar un solo evento, que sea fan.baja.
Eventos
| Evento | Cuándo | Qué hacer |
|---|---|---|
fan.altanombre y correo |
Nació una credencial nueva. | Da de alta a la persona en tu base, si la necesitas ahí. |
fan.bajanombre y correo |
Se dio de baja, su correo rebotó o marcó el envío como spam. | Deja de contactarla. Es el evento que justifica todo esto: si no te enteras, esa persona sigue en tu base sin querer estarlo. |
puntos.cambio |
El saldo de puntos de un fan cambió, venga de donde venga. | Refresca el saldo que muestres en tu terminal. |
codigo.asignado |
Un código de tu inventario quedó en manos de una persona. | Descuéntalo de lo que consideras disponible. |
Los marcados con nombre y correo son una transferencia de datos personales: el dueño del programa tiene que nombrarte en su aviso de privacidad antes de activarlos.
Qué llega
POST https://tu-backend/hooks/gz
X-GZ-Signature: t=1787423400,v1=5d0e04155e4b77636d87f5e06aa7df1d…
X-GZ-Evento: fan.baja
X-GZ-Entrega: 4821
{
"evento": "fan.baja",
"ocurrio": "2026-08-21T18:30:00.000Z",
"datos": {
"correo": "fan@ejemplo.mx",
"motivo": "baja"
}
}
X-GZ-Entrega es el id de la entrega, y el mismo evento reintentado repite ese
id. Úsalo para no procesar dos veces.
Verificar la firma
HMAC-SHA256 sobre {t}.{cuerpo_crudo} con tu secreto (whsec_…),
comparado contra v1.
El timestamp va dentro de lo firmado a propósito. Si solo firmáramos el cuerpo, quien
capture una entrega podría reproducirla mañana con la misma firma válida. Rechaza lo que traiga
un t viejo — cinco minutos es un margen razonable.
Usa el cuerpo tal como llegó, sin deserializar y volver a serializar: un espacio de diferencia cambia el HMAC.
const crypto = require('crypto');
app.post('/hooks/gz', express.raw({ type: 'application/json' }), (req, res) => {
const [, t, v1] = /t=(\d+),v1=([0-9a-f]+)/.exec(req.get('X-GZ-Signature')) || [];
// El cuerpo CRUDO, sin re-serializar: un espacio cambia el HMAC.
const esperado = crypto.createHmac('sha256', process.env.GZ_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest('hex');
if (!v1 || !crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado))) return res.sendStatus(401);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401); // anti-replay
const evento = JSON.parse(req.body);
// Responde rápido y procesa aparte: cualquier 2xx nos basta.
res.sendStatus(200);
encolar(evento, req.get('X-GZ-Entrega'));
});
[HttpPost("/hooks/gz")]
public async Task<IActionResult> Recibir()
{
using var sr = new StreamReader(Request.Body);
var cuerpo = await sr.ReadToEndAsync(); // crudo, sin deserializar
var m = Regex.Match(Request.Headers["X-GZ-Signature"], @"t=(\d+),v1=([0-9a-f]+)");
if (!m.Success) return Unauthorized();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(_secreto));
var esperado = Convert.ToHexString(
h.ComputeHash(Encoding.UTF8.GetBytes($"{m.Groups[1].Value}.{cuerpo}"))).ToLower();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(esperado), Encoding.UTF8.GetBytes(m.Groups[2].Value)))
return Unauthorized();
var edad = Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(m.Groups[1].Value));
if (edad > 300) return Unauthorized(); // anti-replay
Encolar(cuerpo, Request.Headers["X-GZ-Entrega"]);
return Ok();
}
Entrega y reintentos
- Encolamos y entregamos desde un proceso aparte: tu latencia nunca frena a un invitado del portal. Puedes tardar sin romper nada de nuestro lado.
- Cualquier 2xx cuenta como entregado. No leemos tu respuesta.
- Hasta 6 intentos con espera creciente — 1 min, 5 min, 30 min, 2 h y 6 h: unas nueve horas en total.
- Después queda marcado como fallido a la vista del dueño del programa, que puede reencolarlo con un botón cuando arregles tu endpoint. Nada se pierde en silencio.
- Solo HTTPS. La firma protege de manipulación, no de lectura, y el cuerpo lleva correos.
Cómo se activa
Lo configura el dueño del programa en su panel: tu URL, los eventos que quieres, y el secreto —que se muestra una sola vez—. Pídeselo con esos tres datos a la mano.