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 se cerró sin pagarse: venció (sale al llegar expiresAt, aunque nadie vuelva a abrirla), agotó sus intentos (maxFailures) o la expiraste tú (POST /v1/sessions/{reference}/expire). |
Eventos opcionales — solo Credicard
| Evento | Cuándo se emite |
|---|---|
card.authentication_expiring | La verificación de una tarjeta guardada está por vencer (avisos a 15, 7 y 1 día). |
card.authentication_expired | Credicard pidió verificar una tarjeta guardada de nuevo: no se puede cobrar sin el cliente hasta que pague una vez con ella. |
Aplican solo si cobras con Credicard. Algunas tarjetas —el débito nacional, la Master Debit— piden una verificación del tarjetahabiente: un código de su banco (OTP) o un cargo temporal cuyo monto confirma. Credicard la vuelve a pedir cada 3 meses. Estos eventos te avisan para que le pidas al cliente un pago desde el link y la verificación se renueve.
Son opcionales: no vienen incluidos en «todos los eventos». Llegan solo a los
endpoints que los listan en enabledEvents (o que los marcan en Webhooks en tu
panel). Así una integración que no los conoce nunca recibe un tipo de evento
inesperado.
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 (salvo los opcionales de Credicard, que se piden por nombre). |
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,
"processorReference": "000000000004663"
}
}Cuerpo de data según el evento:
| Evento | data |
|---|---|
charge.* | { transactionReference, amountMinor, currency, status, declineReason, errorCode, processorReference } |
card.stored | { seifToken, brand, lastFour, customerRef } |
card.authentication_expiring | { seifToken, brand, lastFour, customerRef, gateway, authenticationStatus, authenticatedAt, estimatedExpiresAt, daysLeft } |
card.authentication_expired | { seifToken, brand, lastFour, customerRef, gateway, authenticationStatus, authenticatedAt, estimatedExpiresAt, transactionReference } |
session.expired | { sessionReference, reason, failureCount, maxFailures } |
Los eventos charge.* traen además el origen del cobro: sessionReference si vino del checkout, o seifToken si fue un cobro con tarjeta guardada.
En los eventos card.authentication_*, estimatedExpiresAt es una estimación de
Seif (tres meses después de la última verificación): quien decide es Credicard, y un
cobro antes de esa fecha puede pedir verificación igual. transactionReference es el
cobro que Credicard rechazó pidiendo la verificación.
En session.expired, reason es expired (venció por tiempo), max_failures_reached (agotó sus intentos) o merchant_request (la expiraste tú): los mismos valores que closedReason en Sesiones de pago. maxFailures es null si la sesión no tenía límite de intentos.
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.
Y al revés: en charge.approved viene processorReference, la referencia que asigna el banco. Es la que te sirve para conciliar contra tu estado de cuenta. En un rechazo va null, porque no hubo autorización que referenciar.
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.