Esta página reúne las recomendaciones clave para construir una integración robusta, confiable y segura con los webhooks de Forpay.
Idempotencia
Forpay puede reenviar la misma notificación más de una vez. Esto ocurre porque el sistema de webhooks garantiza la entrega reintenatando notificaciones que no recibieron una respuesta 2xx. También puede ocurrir en procesos de renotificación manual (ver sección siguiente).
Por ello, tu endpoint debe ser idempotente: procesar el mismo evento múltiples veces debe producir exactamente el mismo resultado que procesarlo una sola vez.
Cómo implementarlo:
Usa el campo installment.internalId (o la combinación product.internalId + installment.id) como clave única para identificar un evento. Antes de actualizar el estado de una cuota en tu base de datos, verifica si ya procesaste ese ID anteriormente.
// Ejemplo de lógica idempotente
async function handlePaymentWebhook(payload) {
const key = `installment_${payload.installment.internalId}`;
const alreadyProcessed = await db.eventLog.findOne({ key });
if (alreadyProcessed) {
// Ya procesado: responder 200 sin hacer nada más
return { status: 200, message: 'Already processed' };
}
// Procesar el evento por primera vez
await db.installments.update({ ... });
await db.eventLog.create({ key, processedAt: new Date() });
return { status: 200 };
}Renotificaciones
Forpay puede renotificar una cuota en distintos estados, incluyendo:
- Cuotas pagadas (
installment.state.id = 2) - Cuotas rechazadas (
installment.state.id = -99) - Cuotas en proceso de cobro (
installment.state.id = 9) - Cuotas en otros estados intermedios (en validación, reversadas, etc.)
Tu sistema no debe limitar el procesamiento a un único estado ni bloquear notificaciones repetidas. En cambio, actualiza siempre el estado de la cuota al valor más reciente recibido, respetando la idempotencia por cada combinación de internalId + state.id.
Una buena estrategia es almacenar el último estado recibido y la fecha del evento, y actualizarlo si la notificación entrante corresponde a un evento más reciente:
async function upsertInstallmentState(payload) {
const { internalId, state, dateTime } = payload.installment;
const existing = await db.installments.findOne({ internalId });
if (!existing || new Date(dateTime) > new Date(existing.dateTime)) {
await db.installments.upsert({
internalId,
stateId: state.id,
stateName: state.name,
dateTime,
});
}
}Respuesta rápida al webhook
Tu endpoint debe responder con un HTTP 200 lo antes posible, idealmente antes de realizar cualquier procesamiento pesado (consultas a base de datos, envío de correos, llamadas a terceros, etc.).
Si el procesamiento tarda demasiado, Forpay puede interpretar la solicitud como fallida y reintentarla.
Patrón recomendado: responder primero, procesar después.
app.post('/webhook/forpay', async (req, res) => {
// 1. Responder de inmediato
res.status(200).json({ received: true });
// 2. Procesar en segundo plano (async, queue, worker, etc.)
processWebhookAsync(req.body).catch(console.error);
});Validar siempre la firma HMAC
Nunca omitas la validación de la firma signature, incluso en ambientes de desarrollo. Esta verificación es la única garantía de que la notificación proviene de Forpay y no de un tercero.
Si la firma no coincide, descarta el evento y devuelve un HTTP 400 o HTTP 401. No actualices ningún estado en tu sistema basándote en una notificación no verificada.
app.post('/webhook/forpay', (req, res) => {
const isValid = validateHmac(req.body, HMAC_SECRET, req.body.signature);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
res.status(200).json({ received: true });
processWebhookAsync(req.body).catch(console.error);
});Ver Verificación HMAC para el detalle de implementación.
No confiar únicamente en el webhook para confirmar pagos
El webhook es el mecanismo principal de notificación, pero pueden ocurrir demoras, pérdidas de conectividad o fallos temporales en tu servidor. Por eso se recomienda implementar una conciliación periódica consultando el estado de las cuotas directamente a la API de Forpay (GET /mandate/{mandateTypeId}/{mandateId}), especialmente para cuotas críticas o de alto monto.
Manejo de errores y reintentos en tu lado
Si tu endpoint falla o retorna un código de error, Forpay reintentará la notificación. Sin embargo, no debes depender exclusivamente de este comportamiento. Implementa un mecanismo de registro de fallos en tu sistema para poder reprocesar notificaciones que no pudieron procesarse correctamente.
async function processWebhookAsync(payload) {
try {
await upsertInstallmentState(payload);
} catch (err) {
// Registrar el fallo para revisión o reprocesamiento manual
await db.failedWebhooks.create({
payload: JSON.stringify(payload),
error: err.message,
receivedAt: new Date(),
});
}
}Seguridad del endpoint
- Usa HTTPS en tu URL de webhook. Forpay no enviará notificaciones a URLs inseguras (
http://). - Evita exponer en logs el contenido completo del payload, ya que puede contener datos sensibles del cliente (RUT, número de cuenta).
- Si es posible, restringe el acceso al endpoint a las IPs de Forpay (consulta a tu ejecutivo de integración la lista de IPs permitidas).
Resumen de buenas prácticas
| Práctica | Descripción |
|---|---|
| Idempotencia | Usa installment.internalId como clave única. No proceses el mismo evento dos veces. |
| Aceptar renotificaciones | No bloquees estados repetidos ni limites a un solo tipo de estado. |
| Responder rápido | Devuelve 200 antes de procesar. Usa colas o workers para el procesamiento. |
| Validar HMAC | Siempre verifica signature antes de procesar el evento. |
| Conciliación activa | Consulta el estado de cuotas por API como respaldo ante fallos de webhook. |
| Registro de fallos | Almacena notificaciones no procesadas para reprocesamiento manual. |
| HTTPS y seguridad | Usa HTTPS, evita loguear datos sensibles, restringe IPs si es posible. |

