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.
Autenticación saliente (opcional)
Además de la firma Seif-Signature (que va siempre), puedes pedir que Seif autentique la llamada a tu endpoint para que tu receptor la valide. Se configura con authType al crear o actualizar:
authType | Campos | Header que Seif envía |
|---|---|---|
none (default) | — | (solo la firma) |
bearer | authToken | Authorization: Bearer <authToken> |
basic | authUsername, authPassword | Authorization: Basic base64(usuario:contraseña) |
api_key | authHeaderName, authToken | <authHeaderName>: <authToken> |
oauth2 | oauthTokenUrl, oauthClientId, oauthClientSecret, oauthScope (opc.) | Authorization: Bearer <token> — Seif obtiene el token con client_credentials y lo cachea |
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", "authType": "bearer", "authToken": "tu-token" }'Las credenciales se guardan cifradas y nunca se devuelven: las respuestas solo
indican hasAuthToken, hasBasicAuth, etc. Para rotarlas, envía el nuevo valor con
PATCH. La firma HMAC sigue activa con cualquier authType.
Gestionar endpoints
GET /v1/webhooks/endpoints # listar
GET /v1/webhooks/endpoints/{id} # ver uno
PATCH /v1/webhooks/endpoints/{id} # actualizar (URL, eventos, estado, auth)
DELETE /v1/webhooks/endpoints/{id} # eliminarEn PATCH, omite un campo para conservarlo; en una credencial, envía "" para borrarla. Cambiar status a disabled detiene los envíos sin borrar el endpoint (el historial de entregas se conserva).
Probar un endpoint (ping)
POST /v1/webhooks/endpoints/{id}/testEncola un evento sintético webhook.test hacia ese endpoint (con su firma y autenticación), aunque esté inactivo o no suscrito a ese evento. Devuelve { deliveryId, eventId }; el resultado aparece en el historial de entregas. Úsalo para verificar conectividad, firma y autenticación sin esperar a una transacción real.
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",
"declineReason": null,
"errorCode": null
}
}Cuerpo de data según el evento:
| Evento | data |
|---|---|
charge.* | { transactionReference, amountMinor, currency, status, declineReason, errorCode } |
card.stored | { seifToken, brand, lastFour, customerRef } |
session.expired | { sessionReference } |
Los eventos charge.* traen además el origen del cobro: sessionReference si vino del checkout, o seifToken si fue un cobro con tarjeta guardada.
Por qué falló un cobro
En charge.declined y charge.failed vienen los dos campos que explican el resultado, así no hace falta consultar la transacción aparte:
| Campo | Para qué |
|---|---|
declineReason | Texto en español, apto para mostrarle al comprador. |
errorCode | Código del procesador. Es el que debes usar para ramificar en tu código — el texto puede cambiar. |
Ambos son null en charge.approved.
Si necesitas más detalle del que trae el webhook (el motivo técnico completo, la
traza de eventos), consulta
GET /v1/transactions/{reference} con la
transactionReference del evento.
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.