Guía Stripe
Falla una tarjeta en Stripe,
tu cliente recibe un WhatsApp.
Un solo webhook escucha invoice.paid, invoice.payment_failed y customer.subscription.trial_will_end, y envía el recibo, el recordatorio de pago o el aviso de fin de prueba por email, WhatsApp o SMS desde tus propias cuentas. Gratis, con el código para Next.js.
¿Con prisa? Resume este artículo con una IA
La respuesta corta
¿Cómo enviar un email, un WhatsApp o un SMS desde un webhook de Stripe?
Crea un endpoint de webhook, verifica cada evento con stripe.webhooks.constructEvent sobre el cuerpo sin procesar, responde 2xx al momento y envía el mensaje con una llamada POST /message. Stripe envía customer.subscription.trial_will_end 3 días antes de que acabe una prueba e invoice.payment_failed tras cada intento fallido: cada evento da un mensaje.
Stripe ya hace parte del trabajo. Smart Retries reintenta un pago de suscripción fallido, hasta 8 veces en 2 semanas por defecto en las cuentas nuevas, y los emails a clientes de Stripe pueden avisar de un pago fallido, de una prueba que acaba en 7 días o de una tarjeta que caduca. Solo por email.
Un email sobre una tarjeta rechazada se pierde fácilmente. Un WhatsApp o un SMS desde un número que el cliente reconoce se lee, y el enlace a la página de factura alojada le permite pagar en un minuto. Tu webhook decide el canal y el momento: email y WhatsApp en el primer fallo, SMS en los siguientes intentos.
Stripe puede entregar el mismo evento dos veces y en cualquier orden, y reintenta un endpoint en producción durante hasta 3 días si no recibe un 2xx. Tu handler tiene que ser rápido e idempotente: procesa cada event.id una sola vez.
Lee también: las notificaciones transaccionales por WhatsApp, y cómo evitar un bloqueo de WhatsApp.

Paso a paso
¿Cómo se construye, paso a paso?
Cinco pasos en un proyecto Next.js App Router, unos 20 minutos. Cualquier backend funciona igual: verificar, responder 2xx, enviar.
Reúne los tres secretos
Tu clave secreta de Stripe, el secreto de firma (whsec_) de tu endpoint de webhook y una clave API de Unlimited Messaging cuando tengas conectado tu buzón, tu número de WhatsApp o tu móvil Android. Los tres se quedan en el servidor, nunca con el prefijo NEXT_PUBLIC_.
.env.local.env.local# .env.local (server only, never NEXT_PUBLIC_) STRIPE_SECRET_KEY=sk_test_xxxx # Printed by "stripe listen" in dev, or shown on the endpoint in the Dashboard STRIPE_WEBHOOK_SECRET=whsec_xxxx UM_API_KEY=um_live_xxxxAñade el helper notify
Una función solo de servidor alrededor de POST /message. Omite los canales sin destinatario y marca el mensaje como transaccional, para que pase por delante de cualquier envío masivo que espere en tu cola.
lib/notify.tsTypeScript// lib/notify.ts import 'server-only'; export type Channel = 'EMAIL' | 'WHATSAPP' | 'SMS'; export async function notify(input: { email?: string; phone?: string; // E.164, e.g. +33612345678 channels: Channel[]; subject: string; text: string; }) { const channels = input.channels .filter((c) => (c === 'EMAIL' ? input.email : input.phone)) .map((channel) => ({ channel })); if (channels.length === 0) return null; const res = await fetch('https://api.unlimitedmessaging.app/message', { method: 'POST', headers: { Authorization: `Bearer ${process.env.UM_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: { email: input.email, phone: input.phone }, channels, subject: input.subject, text: input.text, priority: 'transactional', }), }); if (!res.ok) throw new Error(`Unlimited Messaging ${res.status}: ${await res.text()}`); return res.json(); }Crea la ruta del webhook
Lee el cuerpo sin procesar con req.text(): Stripe firma los bytes exactos, y parsear el JSON antes rompe la verificación. constructEvent también rechaza los eventos más antiguos que su tolerancia por defecto de 5 minutos. Responde 200 al momento y envía dentro de after().
app/api/stripe/webhook/route.tsRoute Handler// app/api/stripe/webhook/route.ts import type Stripe from 'stripe'; import { after } from 'next/server'; import { stripe, handleStripeEvent } from '@/lib/stripe-notify'; export async function POST(req: Request) { // The raw body: parsing it as JSON first breaks the signature check const body = await req.text(); const signature = req.headers.get('stripe-signature') ?? ''; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!); } catch { return new Response('Invalid signature', { status: 400 }); } // Answer Stripe with a 2xx right away, send the messages after the response after(() => handleStripeEvent(event).catch(console.error)); return new Response('ok', { status: 200 }); }Asocia cada evento a un mensaje
invoice.paid envía un recibo por email. invoice.payment_failed envía email y WhatsApp con el enlace de la factura alojada, y añade el SMS cuando attempt_count pasa de 1. customer.subscription.trial_will_end llega 3 días antes del fin de la prueba: recupera el cliente para el email y el teléfono.
lib/stripe-notify.tsTypeScript// lib/stripe-notify.ts import 'server-only'; import Stripe from 'stripe'; import { notify, type Channel } from '@/lib/notify'; export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const money = (amount: number, currency: string) => new Intl.NumberFormat('en', { style: 'currency', currency }).format(amount / 100); export async function handleStripeEvent(event: Stripe.Event) { // Stripe can send the same event twice: skip the ones already handled if (!(await markProcessed(event.id))) return; switch (event.type) { case 'invoice.paid': { const inv = event.data.object; await notify({ email: inv.customer_email ?? undefined, channels: ['EMAIL'], subject: 'Payment received', text: `Thanks, we received ${money(inv.amount_paid, inv.currency)}.`, }); break; } case 'invoice.payment_failed': { const inv = event.data.object; // First failure: email + WhatsApp. Next retries: SMS as well. const channels: Channel[] = inv.attempt_count > 1 ? ['EMAIL', 'WHATSAPP', 'SMS'] : ['EMAIL', 'WHATSAPP']; await notify({ email: inv.customer_email ?? undefined, phone: inv.customer_phone ?? undefined, channels, subject: 'Your payment did not go through', text: `Your payment of ${money(inv.amount_due, inv.currency)} failed. Update your card here: ${inv.hosted_invoice_url}`, }); break; } case 'customer.subscription.trial_will_end': { const sub = event.data.object; // sent 3 days before the trial ends const customer = await stripe.customers.retrieve(sub.customer as string); if (customer.deleted) break; await notify({ email: customer.email ?? undefined, phone: customer.phone ?? undefined, channels: ['EMAIL', 'WHATSAPP'], subject: 'Your trial ends in 3 days', text: 'Your trial ends in 3 days. Nothing to do if your card is up to date.', }); break; } } } async function markProcessed(eventId: string): Promise<boolean> { // e.g. INSERT INTO stripe_events (id) VALUES ($1) ON CONFLICT DO NOTHING, // then return true only when a row was inserted return true; }Prueba con la CLI de Stripe y pasa a producción
stripe listen reenvía los eventos de prueba a localhost y muestra el secreto de firma que debes usar. Dispara un pago fallido, revisa los mensajes y añade tu URL de producción como endpoint en el Dashboard con los tres eventos y su propio secreto whsec_.
TerminalStripe CLI# Forward test events to your local route (prints a whsec_ secret) stripe listen --forward-to localhost:3000/api/stripe/webhook # In another terminal: fire a failed payment stripe trigger invoice.payment_failed
Vibe coding
¿Programas con Cursor, Lovable o Claude Code? Pega este prompt
El prompt le da a tu editor de IA el patrón seguro: verificación de firma sobre el cuerpo sin procesar, 2xx rápido, handler idempotente, mensajes por canal. Te pide tus contactos de prueba antes de enviar.
Añade notificaciones a clientes en los eventos de Stripe a este proyecto Next.js (App Router) con Unlimited Messaging: email, WhatsApp y SMS enviados desde mis propias cuentas.
1. Usa STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET y UM_API_KEY desde .env.local, y añádelas a .env.example. No uses nunca un prefijo NEXT_PUBLIC_.
2. Crea lib/notify.ts (import 'server-only') que exporte notify({ email, phone, channels, subject, text }) y envíe un POST https://api.unlimitedmessaging.app/message con la cabecera Authorization: Bearer UM_API_KEY y el JSON { to: { email, phone }, channels: [{ channel: 'EMAIL' | 'WHATSAPP' | 'SMS' }], subject, text, priority: 'transactional' }. Omite los canales sin destinatario.
3. Crea app/api/stripe/webhook/route.ts: lee el cuerpo sin procesar con req.text(), verifícalo con stripe.webhooks.constructEvent y la cabecera stripe-signature, devuelve 400 si falla, devuelve 200 al momento y procesa el evento dentro de after() de next/server.
4. Gestiona invoice.paid (recibo por email), invoice.payment_failed (email + WhatsApp con hosted_invoice_url, añade SMS cuando attempt_count > 1) y customer.subscription.trial_will_end (recupera el cliente, email + WhatsApp). Guarda event.id en una tabla con una restricción de unicidad y omite los eventos ya procesados.
5. Referencia de la API: https://docs.unlimitedmessaging.app. Webhooks de Stripe: https://docs.stripe.com/webhooks.
Enséñame el diff y los comandos stripe listen / stripe trigger para probar, y pregúntame mi email y mi número de prueba antes de enviar nada.Las opciones
¿Los emails integrados de Stripe o tus propios mensajes por webhook?
Los emails a clientes de Stripe no necesitan código. Un webhook añade los canales y el calendario que elijas. Se combinan bien.
| Emails a clientes de StripeIntegrados en Billing | Webhook + API de emailPor ejemplo Resend | Webhook + TwilioAPI de SMS | Webhook + Unlimited MessagingEmail, WhatsApp y SMS | |
|---|---|---|---|---|
| Canales | SMS, WhatsApp con plantillas de Meta | Email, WhatsApp y SMS en una petición | ||
| Pago fallido | Un email tras cada pago con tarjeta fallido | Tu email, tu calendario | Tu SMS, tu calendario | Escalar del email a WhatsApp y SMS |
| Fin de la prueba | Email de aviso 7 días antes | Evento 3 días antes, tu email | Evento 3 días antes, tu SMS | Evento 3 días antes, cualquier canal |
| Coste de los mensajes | Incluido en Stripe Billing | Gratis hasta 3.000 emails al mes, 100 al día | Se cobra por segmento de SMS | Plan Free, sin coste por mensaje |
| Código | Ninguno, ajustes del Dashboard | Un handler de webhook | Un handler de webhook | Un handler de webhook |
| Remitente | Stripe, con tu marca | Tu dominio verificado | Un número de Twilio | Tu propio buzón, tu número de WhatsApp y tu móvil |
Comprobado el 9 de noviembre de 2026 en la documentación de Stripe, resend.com/pricing y twilio.com (enlaces en las fuentes). Puedes mantener los emails de Stripe y añadir WhatsApp o SMS encima.
La letra pequeña
¿Cuáles son los límites?
Gratis quita la factura por mensaje. Los webhooks siguen pidiendo cuidado, y WhatsApp y el SMS siguen teniendo sus reglas.
Los teléfonos en Stripe son texto libre
customer_phone y customer.phone guardan lo que se escribió. WhatsApp y el SMS necesitan formato E.164 (+34612345678): normaliza los números al recogerlos, por ejemplo con la recogida de teléfono de Checkout, y omite los canales de teléfono si falta el número.
Responder 200 primero significa que los reintentos son cosa tuya
after() envía los mensajes cuando Stripe ya tiene su 2xx, así que Stripe no reenviará si tu envío falla. Para mensajes que no puedes perder, guarda el evento y procésalo desde una cola que reintente.
Ni duplicados ni orden están garantizados
Stripe puede entregar un evento más de una vez y no garantiza el orden. Deduplica por event.id, no por created, y recupera el objeto actualizado desde la API cuando importe el estado actual.
WhatsApp es no oficial: recordatorios sí, campañas no
Tu número se vincula como un dispositivo, fuera de la plataforma Business de Meta. Un recordatorio de pago a tu propio cliente es esperado; una campaña a todos tus antiguos clientes es el tipo de tráfico que hace que bloqueen un número.
El SMS necesita un móvil Android encendido
Los SMS salen de la SIM de tu móvil, con tu tarifa. Android pide confirmación a partir de 30 SMS en 30 minutos, así que la app espacia los envíos a unos 30 cada 30 minutos: de sobra para reclamaciones, no para una campaña masiva.
Los mensajes del plan Free llevan una firma
En el plan Free, los mensajes terminan con una línea corta "Sent with UnlimitedMessaging.app", y los archivos multimedia y los webhooks de estado son de pago. Medium y Max quitan la línea.
FAQ
Preguntas sobre las notificaciones por webhook de Stripe
Fuentes
Fuentes y referencias
Para verificar por ti mismo la información de este artículo:
- Using webhooks with subscriptions (trial_will_end 3 días antes, invoice.payment_failed)Stripe Docs
- Receive Stripe events in your webhook endpoint (firmas, 2xx, duplicados, tolerancia de 5 minutos)Stripe Docs
- Automate payment retries (Smart Retries: hasta 8 reintentos en 2 semanas por defecto)Stripe Docs
- Automate customer emails (pagos fallidos, aviso 7 días antes del fin de la prueba)Stripe Docs
- after (estable desde la v15.1.0)Next.js Docs
- route.js (Route Handlers)Next.js Docs
- Pricing (Free: 3.000 emails al mes, 100 al día)Resend
- Precios de SMS, por segmento y por paísTwilio
- SmsUsageMonitor (umbral de 30 SMS en 30 minutos)Android Open Source Project
- Referencia de la API: POST /messageUnlimited Messaging Docs

Escrito por
Ilian · Ingeniero de software senior
Ingeniero de software senior, funda y dirige varias empresas tecnológicas. Diseña productos en torno a las API de mensajería, la automatización y la integración de la IA, y comparte aquí lo que aprende construyendo.
Convierte hoy los pagos fallidos en recordatorios por WhatsApp.
Crea una cuenta gratis, conecta un buzón, un número de WhatsApp o un móvil Android y pega el handler. Sin tarjeta, sin coste por mensaje.