Concepto clave
Los webhooks de Stripe son notificaciones HTTP que Stripe envía a tu servidor cuando ocurren eventos importantes en tu cuenta, como un pago exitoso, una suscripción cancelada o una factura generada. Imagina que son como mensajeros que llegan a tu puerta cada vez que algo relevante sucede en el sistema de pagos, permitiéndote reaccionar en tiempo real sin tener que consultar constantemente la API.
La seguridad es crítica porque estos webhooks contienen datos sensibles sobre transacciones y clientes. Un endpoint inseguro podría permitir que atacantes falsifiquen eventos o accedan a información confidencial. En la práctica, esto se protege verificando la firma digital que Stripe incluye en cada solicitud, similar a cómo un sello oficial autentica un documento importante.
Cómo funciona en la práctica
Cuando configuras un webhook en el dashboard de Stripe, proporcionas una URL de tu servidor (por ejemplo, https://tudominio.com/api/webhooks/stripe). Stripe enviará una solicitud POST a esta URL cada vez que ocurra un evento al que te hayas suscrito. Tu servidor debe:
- Recibir la solicitud y extraer el cuerpo (payload) y los encabezados
- Verificar la firma usando tu clave secreta de webhook
- Procesar el evento según su tipo
- Responder con un código de estado HTTP 200 para confirmar la recepción
Un flujo típico para una suscripción sería: 1) Cliente completa el pago inicial, 2) Stripe envía evento customer.subscription.created, 3) Tu endpoint recibe el evento, 4) Actualiza tu base de datos para activar el acceso del cliente.
Código en acción
Aquí tienes un ejemplo básico en Node.js usando Express:
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();
// Middleware para parsear JSON
app.use(express.json());
app.post('/api/webhooks/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
try {
// Verificar la firma del webhook
const event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Manejar diferentes tipos de eventos
switch (event.type) {
case 'customer.subscription.created':
await handleSubscriptionCreated(event.data.object);
break;
case 'invoice.payment_succeeded':
await handlePaymentSucceeded(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionDeleted(event.data.object);
break;
default:
console.log(`Evento no manejado: ${event.type}`);
}
res.json({received: true});
} catch (err) {
console.error('Error en webhook:', err.message);
res.status(400).send(`Webhook Error: ${err.message}`);
}
});
async function handleSubscriptionCreated(subscription) {
// Aquí actualizarías tu base de datos
console.log(`Suscripción creada: ${subscription.id}`);
// Ejemplo: activar acceso del usuario en tu sistema
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Servidor escuchando en puerto ${PORT}`);
});Y aquí una versión mejorada con manejo de errores más robusto:
// Versión mejorada con retry logic y logging
app.post('/api/webhooks/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
const eventId = req.headers['stripe-event-id'] || 'unknown';
// Log inicial para debugging
console.log(`Recibiendo webhook ${eventId}`);
try {
const event = stripe.webhooks.constructEvent(
req.rawBody || JSON.stringify(req.body),
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
// Verificar que no hayamos procesado este evento antes
if (await isEventProcessed(event.id)) {
console.log(`Evento ${event.id} ya procesado, ignorando`);
return res.json({received: true});
}
// Procesar en background para responder rápido a Stripe
processEventAsync(event);
// Marcar como procesado
await markEventAsProcessed(event.id);
res.json({received: true});
} catch (err) {
// Log detallado del error
console.error(`Error en webhook ${eventId}:`, {
error: err.message,
signature: sig,
body: req.body
});
// Diferenciar entre errores de firma y otros errores
if (err.message.includes('Signature')) {
res.status(401).send('Firma inválida');
} else {
res.status(400).send(`Error: ${err.message}`);
}
}
});Errores comunes
- No verificar la firma del webhook: Esto expone tu endpoint a ataques de falsificación. Siempre usa
stripe.webhooks.constructEvent()o su equivalente en tu lenguaje. - Usar el mismo secret para diferentes entornos: Genera secrets separados para desarrollo, staging y producción en el dashboard de Stripe.
- No manejar eventos duplicados: Stripe puede reenviar eventos. Implementa idempotencia almacenando IDs de eventos procesados.
- Procesamiento síncrono largo: Si tu lógica tarda más de 10 segundos, Stripe podría reintentar. Procesa en background y responde inmediatamente.
- No probar con eventos reales: Usa la CLI de Stripe o el modo test para enviar eventos de prueba antes de ir a producción.
Checklist de dominio
- ✅ Configuré un endpoint webhook en el dashboard de Stripe con la URL correcta
- ✅ Implementé verificación de firma usando el webhook secret
- ✅ Manejo al menos 3 tipos de eventos relevantes para mi negocio
- ✅ Mi endpoint responde en menos de 5 segundos para evitar timeouts
- ✅ Implementé idempotencia para evitar procesamiento duplicado
- ✅ Tengo logging adecuado para debugging de eventos
- ✅ Probé mi endpoint con la CLI de Stripe y eventos reales
Implementa un endpoint de webhook seguro para manejar suscripciones
En este ejercicio, crearás un endpoint de webhook seguro que maneje eventos críticos de suscripciones. Sigue estos pasos:
- Configura tu entorno: Crea un proyecto Node.js/Express básico e instala el SDK de Stripe (
npm install stripe express). Configura las variables de entorno paraSTRIPE_SECRET_KEYySTRIPE_WEBHOOK_SECRET. - Crea el endpoint: Implementa una ruta POST en
/api/webhooks/stripeque:- Reciba solicitudes POST con JSON
- Extraiga el header
stripe-signature - Verifique la firma usando
stripe.webhooks.constructEvent() - Responda con 200 si es válido, 401 si la firma es inválida
- Implementa manejo de eventos: Añade lógica para procesar estos eventos:
customer.subscription.created: Registrar en consola "Suscripción creada" + IDinvoice.payment_succeeded: Registrar "Pago exitoso" + montocustomer.subscription.deleted: Registrar "Suscripción cancelada" + razón
- Añade idempotencia: Implementa un sistema simple (puede ser en memoria) para evitar procesar el mismo evento dos veces.
- Prueba con la CLI: Instala la CLI de Stripe y ejecuta
stripe listen --forward-to localhost:3000/api/webhooks/stripepara probar eventos reales.
Entrega: Código completo del endpoint y captura de pantalla mostrando eventos procesados correctamente.
Pistas- Recuerda que req.body debe ser el string RAW, no el objeto parseado, para la verificación de firma
- Puedes usar un Set o un objeto simple en memoria para almacenar IDs de eventos procesados temporalmente
- La CLI de Stripe necesita autenticación con
stripe loginantes de poder usarla
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.