Saltar al contenido principal

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.

Dibujo de la aplicación Unlimited Messaging que muestra una lista de mensajes con su estado (leído, entregado, enviado, en cola), con el titular «Cobros de Stripe por WhatsApp, SMS y email».

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.

  1. 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_xxxx
  2. Añ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();
    }
  3. 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 });
    }
  4. 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;
    }
  5. 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.

Prompt para copiar
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.

¿Los emails integrados de Stripe o tus propios mensajes por webhook?
Emails a clientes de StripeIntegrados en BillingWebhook + API de emailPor ejemplo ResendWebhook + TwilioAPI de SMSWebhook + Unlimited MessagingEmail, WhatsApp y SMS
CanalesEmailEmailSMS, WhatsApp con plantillas de MetaEmail, WhatsApp y SMS en una petición
Pago fallidoUn email tras cada pago con tarjeta fallidoTu email, tu calendarioTu SMS, tu calendarioEscalar del email a WhatsApp y SMS
Fin de la pruebaEmail de aviso 7 días antesEvento 3 días antes, tu emailEvento 3 días antes, tu SMSEvento 3 días antes, cualquier canal
Coste de los mensajesIncluido en Stripe BillingGratis hasta 3.000 emails al mes, 100 al díaSe cobra por segmento de SMSPlan Free, sin coste por mensaje
CódigoNinguno, ajustes del DashboardUn handler de webhookUn handler de webhookUn handler de webhook
RemitenteStripe, con tu marcaTu dominio verificadoUn número de TwilioTu 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

Ilian, ingeniero de software senior

Escrito por

· 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.

Ver el perfil de LinkedInActualizado el

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.