Webhooks
Selgeo envía notificaciones HTTP POST en tiempo real a su servidor cuando ocurren eventos en su cuenta. Use webhooks para automatizar flujos de trabajo como aprovisionar acceso cuando se aprueba un socio, sincronizar comisiones con su sistema de contabilidad o alertar a su equipo sobre fraudes.
Todos los payloads de webhook usan API versión v1.
Registro de puntos de conexión
Registre puntos de conexión webhook desde la página Configuración > Webhooks en el panel de control del comerciante.
- Haga clic en Agregar punto de conexión.
- Ingrese la URL donde desea recibir eventos. Los puntos de conexión en modo live deben usar HTTPS.
- Seleccione los eventos a los que desea suscribirse.
- Haga clic en Crear. Su secreto de firma (
whsec_...) se muestra una vez — cópielo y guárdelo de forma segura.
Verificación de firma
Cada solicitud de webhook incluye un encabezado X-Selgeo-Signature:
t=<unix_timestamp>,v1=<hmac_hex>
El HMAC se calcula como HMAC-SHA256(signing_secret_bytes, "<timestamp>.<raw_json_body>").
Ejemplos de verificación
- Node.js
- Python
- PHP
import crypto from 'node:crypto';
function verifyWebhookSignature(signingSecret, signatureHeader, rawBody) {
if (!signatureHeader) return false;
const rawSecret = signingSecret.replace(/^whsec_/, '');
const secretBytes = Buffer.from(rawSecret, 'hex');
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=', 2))
);
const timestamp = parts.t;
const receivedHmac = parts.v1;
if (!timestamp || !receivedHmac) return false;
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300 || age < -30) return false;
const expectedHmac = crypto
.createHmac('sha256', secretBytes)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const receivedBuf = Buffer.from(receivedHmac, 'hex');
const expectedBuf = Buffer.from(expectedHmac, 'hex');
if (receivedBuf.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(receivedBuf, expectedBuf);
}
app.post('/webhooks/selgeo', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-selgeo-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature('whsec_YOUR_SIGNING_SECRET', signature, rawBody)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(rawBody);
console.log('Evento recibido:', event.event_name);
res.status(200).send('OK');
});
import hashlib
import hmac
import time
def verify_webhook_signature(signing_secret: str, signature_header: str, raw_body: str) -> bool:
if not signature_header:
return False
raw_secret = signing_secret.removeprefix("whsec_")
secret_bytes = bytes.fromhex(raw_secret)
try:
parts = dict(part.split("=", 1) for part in signature_header.split(","))
except ValueError:
return False
timestamp = parts.get("t")
received_hmac = parts.get("v1")
if not timestamp or not received_hmac:
return False
try:
age = int(time.time()) - int(timestamp)
except ValueError:
return False
if age > 300 or age < -30:
return False
message = f"{timestamp}.{raw_body}".encode()
expected_hmac = hmac.new(secret_bytes, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(received_hmac, expected_hmac)
<?php
function verifyWebhookSignature(string $signingSecret, string $signatureHeader, string $rawBody): bool {
if ($signatureHeader === '') return false;
$rawSecret = str_starts_with($signingSecret, 'whsec_') ? substr($signingSecret, 6) : $signingSecret;
$secretBytes = hex2bin($rawSecret);
if ($secretBytes === false) return false;
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
$segments = explode('=', $part, 2);
if (count($segments) !== 2) continue;
[$key, $value] = $segments;
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? null;
$receivedHmac = $parts['v1'] ?? null;
if (!$timestamp || !$receivedHmac) return false;
$age = time() - (int) $timestamp;
if ($age > 300 || $age < -30) return false;
$expectedHmac = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secretBytes);
return hash_equals($expectedHmac, $receivedHmac);
}
Catálogo de eventos
Eventos de socios
| Evento | Descripción |
|---|---|
participant.created | Un nuevo socio aplicó o fue añadido a un programa |
participant.approved | Un socio fue aprobado |
participant.rejected | Una solicitud de socio fue rechazada |
participant.suspended | Un socio activo fue suspendido |
participant.reinstated | Un socio suspendido fue reinstaurado |
participant.erased | Los datos de un socio fueron borrados (RGPD) |
Eventos de atribución
| Evento | Descripción |
|---|---|
attribution.created | Un clic fue atribuido a un socio |
attribution.converted | Un clic atribuido resultó en una conversión |
attribution.expired | Una ventana de atribución expiró sin conversión |
Eventos de conversión
| Evento | Descripción |
|---|---|
conversion.created | Se registró una nueva conversión |
conversion.fraud_detected | Se detectó fraude (auto-referido o conversión duplicada) |
Eventos de comisión
| Evento | Descripción |
|---|---|
commission.created | Se calculó una nueva comisión |
commission.approved | Se aprobó una comisión para pago |
commission.rejected | Se rechazó una comisión |
commission.refunded | Se revirtió una comisión debido a un reembolso |
Política de reintentos
Si su punto de conexión devuelve un código de estado que no es 2xx o la solicitud expira (30 segundos), Selgeo reintenta con retroceso exponencial:
| Intento | Retraso después del fallo |
|---|---|
| 1er reintento | 1 minuto |
| 2do reintento | 2 minutos |
| 3er reintento | 4 minutos |
| 4to reintento | 15 minutos |
Después de 5 intentos totales, la entrega pasa al estado dead-letter.
Mejores prácticas
- Devolver 200 rápidamente. Procese el evento de forma asíncrona en un trabajo en segundo plano.
- Manejar duplicados. Use el campo
event_idpara deduplicar. - Verificar firmas. Siempre verifique el encabezado
X-Selgeo-Signature. - Usar HTTPS en producción. Los puntos de conexión en modo live requieren HTTPS.