Webhooks
Los webhooks le avisan a tu servidor, en tiempo real, cuando algo ocurre: un cobro aprobado, una tarjeta guardada, una sesión que expiró. Es la forma recomendada de reaccionar a los pagos (mejor que hacer polling).
Eventos disponibles
| Evento | Cuándo se emite |
|---|---|
charge.approved | Un cobro fue aprobado. |
charge.declined | Un cobro fue rechazado por el banco/tarjeta. |
charge.failed | Un cobro no se pudo procesar. |
card.stored | Se almacenó una tarjeta tras un cobro aprobado. |
card.deleted | Se eliminó una tarjeta guardada. |
session.otp_requested | La sesión solicitó un OTP (flujos con segundo factor). |
session.otp_confirmed | El OTP fue confirmado. |
session.expired | La sesión expiró sin pagarse. |
Registrar un endpoint
POST /v1/webhooks/endpointsAutenticación: clave secreta (Authorization: Bearer sk_…).
| Campo | Tipo | Descripción |
|---|---|---|
url | string (https) | A dónde Seif enviará los eventos. Debe ser https. |
enabledEvents | string[] | Eventos a recibir. Omítelo o envía [] para recibir todos. |
description | string | Nota interna (opcional). |
curl -X POST https://api.seif.pagosripei.com/v1/webhooks/endpoints \
-H "Authorization: Bearer sk_test_tu_clave" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.mitienda.com/seif/webhooks",
"enabledEvents": ["charge.approved", "charge.declined", "charge.failed"]
}'La respuesta incluye un secret (whsec_…), mostrado una sola vez. Guárdalo: con él verificas la firma de cada evento.
Formato del evento
Cada entrega es un POST con este cuerpo:
{
"id": "evt_1a2b3c...",
"type": "charge.approved",
"createdAt": "2026-06-09T18:05:12.000Z",
"env": "production",
"data": {
"transactionReference": "txn_2nFp9Zk",
"sessionReference": "cs_8sKd2pQ1Aa",
"amountMinor": 105000,
"currency": "VES",
"status": "approved"
}
}Cuerpo de data según el evento:
| Evento | data |
|---|---|
charge.* | { transactionReference, sessionReference, amountMinor, currency, status } |
card.stored | { seifToken, brand, lastFour, customerRef } |
session.expired | { sessionReference } |
Verificar la firma
Cada entrega trae el header Seif-Signature:
Seif-Signature: t=1717954512,v1=5f8e...c3Recalcula HMAC-SHA256 sobre `${t}.${cuerpoCrudo}` con tu secret (whsec_…) y compara con v1. Verifica además que t sea reciente para rechazar reenvíos.
import crypto from 'crypto';
function verifySeifWebhook(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`, 'utf8')
.digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300; // 5 min
return ok && fresh;
}Verifica la firma usando el cuerpo crudo (sin parsear). Si tu framework parsea el JSON antes, re-serializar cambiará los bytes y la firma no coincidirá.
Entregas y reintentos
- Responde
2xxrápido (idealmente< 5 s). Cualquier otra respuesta se considera fallida. - Seif reintenta hasta 5 veces con backoff exponencial.
- Puedes consultar el historial de entregas y reintentar manualmente:
GET /v1/webhooks/deliveries
GET /v1/webhooks/deliveries/{id}
POST /v1/webhooks/deliveries/{id}/retryDiseña tu endpoint para ser idempotente: usa el id del evento (evt_…)
para no procesar dos veces el mismo evento si llega repetido tras un reintento.