
Un webhook es la forma en que tu tienda, tu panel de facturación o tu bot se entera de que un cliente pagó. SnapEX Pay envía un HTTP POST a tu URL en cada evento de factura, pago y retiro. Bien hecho, es la señal más rápida y fiable que tienes. Hecho a la ligera, es una forma de marcar como pagados pedidos que nunca se pagaron. Esta guía cubre lo que importa.
Configúralo
En el panel del comercio ve a Invoices → la tarjeta Webhook, escribe tu URL y pulsa Save. Una URL por cuenta recibe todos los tipos de evento. Al guardar por primera vez, el panel muestra una sola vez el secreto de firma whsec_…: guárdalo solo en tu servidor.
La URL debe ser una dirección https:// absoluta con un host público: sin login:password, sin localhost, y el host no debe resolver a una IP privada o reservada. Las redirecciones no se siguen, así que indica la URL final.
Qué llega
Cada petición lleva cuatro encabezados:
| Encabezado | Significado |
|---|---|
Webhook-Id |
Id único del evento. No cambia en los reintentos ni en los reenvíos manuales. |
Webhook-Timestamp |
Hora Unix (segundos) en que se envió este intento. Cada reintento tiene un valor nuevo. |
Webhook-Signature |
v1= y el HMAC-SHA256 en hex minúsculas de {Webhook-Timestamp}.{raw body}. |
Content-Type |
Siempre application/json. |
El cuerpo tiene id (el mismo que Webhook-Id), type, created_at y data. En los eventos invoice.*, data es la factura con la misma forma que GET /invoices/{id}, incluidos environment, amount_usd, status y tu external_id.
Los eventos de factura:
| Evento | Cuándo |
|---|---|
invoice.created |
Factura creada, todavía sin método de pago elegido. |
invoice.awaiting |
Se eligió un método de pago y se asignó la dirección. |
invoice.underpaid |
Llegó una parte del importe; la misma dirección espera el resto. |
invoice.paid |
Se pagó y acreditó el importe completo. La única señal para entregar el pedido. |
invoice.partially_paid |
Solo se pagó una parte, terminó el plazo para completar el pago y la parte recibida se te acreditó. |
invoice.expired |
Terminó el plazo de la factura sin pago. |
invoice.extra_payment |
Llegó otra transferencia después de pagada la factura; se acredita por separado. |
Verifica la firma
- Lee el cuerpo crudo de la petición como bytes, antes de cualquier parseo de JSON. Un JSON vuelto a serializar no coincidirá.
- Calcula el HMAC-SHA256 de
{Webhook-Timestamp}.{raw body}con el secreto completo como clave, prefijowhsec_incluido, y codifícalo en hex. - Compara
v1=+ hex con el encabezadoWebhook-Signatureusando una función de tiempo constante. - Rechaza un
Webhook-Timestampque difiera de tu reloj en más de 5 minutos. Los reintentos llevan una marca de tiempo nueva, así que esto no los rompe.
PHP:
$secret = getenv('SNAPEX_WEBHOOK_SECRET'); // whsec_… from the cabinet
$body = file_get_contents('php://input'); // raw body, before json_decode
$ts = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
$expected = 'v1=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(400);
exit;
}
$event = json_decode($body, true);
// skip if $event['id'] was already processed; do the heavy work in a queue
http_response_code(200);
Node.js (Express):
const crypto = require('crypto');
app.post('/hooks/snapex', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('Webhook-Timestamp') || '';
const sig = Buffer.from(req.get('Webhook-Signature') || '');
const expected = Buffer.from('v1=' + crypto
.createHmac('sha256', process.env.SNAPEX_WEBHOOK_SECRET)
.update(ts + '.' + req.body)
.digest('hex'));
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!fresh || sig.length !== expected.length || !crypto.timingSafeEqual(sig, expected)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
// skip if event.id was already processed; queue the heavy work
res.sendStatus(200);
});
Reintentos, duplicados y orden
- Éxito es cualquier 2xx. Cualquier otro código, un timeout o un error de red es un fallo. Los timeouts son de 5 segundos para conectar y 15 segundos para toda la petición, así que responde rápido y haz el trabajo pesado de forma asíncrona.
- Reintentos. Hasta 5 intentos por evento: el primero y luego unos 10 segundos, 1 minuto, 5 minutos y 15 minutos después de cada fallo. Tras el quinto fallo la entrega automática se detiene; el panel lista las entregas recientes y tiene un botón Retry que reenvía el evento con el mismo
Webhook-Id. - Duplicados. La entrega es al menos una vez (at-least-once): el mismo evento puede llegar más de una vez. Guarda los valores de
Webhook-Idya procesados e ignora las repeticiones. - Orden. Los eventos pueden llegar fuera de orden: un evento antiguo reintentado puede llegar después de uno más nuevo. No dependas del orden; compara el estado del evento con el que ya tienes.
- Vuelve a comprobar si quieres. Después de un webhook puedes volver a leer el objeto con la API:
GET /invoices/{id}.
Pruébalo sin dinero real
Crea una factura de prueba ("environment": "test"), págala en la página de pago y mira cómo llegan los webhooks con data.environment = "test". Detalles: Cómo probar pagos cripto sin dinero real.
Los módulos listos para WHMCS, BILLmanager, WooCommerce y otras plataformas ya verifican la firma y nunca acreditan un pago dos veces; consulta pay.snapex.pro. Referencia completa: documentación de la API de SnapEX Pay.