← Volver al catálogo
WebReferenciaIntermedioGratis

Stripe Webhooks: Verificacion y Manejo de Eventos de Pago

Skill de referencia para implementar y verificar webhooks de Stripe en Node.js y Python, cubriendo verificacion de firma HMAC, tipos de eventos de pago/suscripcion y configuracion del entorno local.

Descargar skill (.zip)

Descarga abierta · sin registro · para Node.js, Python, Express

// resultado_de_ejemplo

// Generated with: stripe-webhooks skill // https://github.com/hookdeck/webhook-skills // // CultivaSaaS — Stripe Webhook Handler (Express) // Maneja eventos de pago, suscripcion e invoice con verificacion HMAC-SHA256.

const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const { createClient } = require('@supabase/supabase-js');

const router = express.Router();

// ───────────────────────────────────────────── // Supabase client (server-side, service role) // ───────────────────────────────────────────── const supabase = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY );

// ───────────────────────────────────────────── // Idempotency: set de IDs procesados en memoria // (produccion: usar Redis o tabla stripe_events) // ───────────────────────────────────────────── const processedEvents = new Set();

// ───────────────────────────────────────────── // POST /webhooks/stripe // ───────────────────────────────────────────── router.post( '/webhooks/stripe', express.raw({ type: 'application/json' }), // raw body ANTES de JSON.parse async (req, res) => { const sig = req.headers['stripe-signature'];

// 1. VERIFICAR FIRMA ─────────────────────
let event;
try {
  event = stripe.webhooks.constructEvent(
    req.body,                               // Buffer sin parsear
    sig,
    process.env.STRIPE_WEBHOOK_SECRET       // whsec_… del dashboard
  );
} catch (err) {
  console.error(`[stripe] Signature verification failed: ${err.message}`);
  return res.status(400).send(`Webhook Error: ${err.message}`);
}

// 2. IDEMPOTENCIA ────────────────────────
if (processedEvents.has(event.id)) {
  console.log(`[stripe] Event ${event.id} already processed, skipping.`);
  return res.status(200).json({ received: true, skipped: true });
}
processedEvents.add(event.id);

// 3. DISPATCH DE EVENTOS ─────────────────
try {
  switch (event.type) {

    // Checkout completado → activar cuenta + enviar bienvenida
    case 'checkout.session.completed': {
      const session = event.data.object;
      const customerId = session.customer;
      const email      = session.customer_email ?? session.customer_details?.email;
      const priceId    = session.line_items?.data?.[0]?.price?.id ?? null;

      await supabase.from('accounts').upsert({
        stripe_customer_id: customerId,
        email,
        plan: resolvePlan(priceId),
        status: 'active',
        activated_at: new Date().toISOString(),
      }, { onConflict: 'stripe_customer_id' });

      // Disparar email de bienvenida (via Resend / cola interna)
      await enqueueEmail({ type: 'welcome', email, plan: resolvePlan(priceId) });

      console.log(`[stripe] checkout.session.completed → account ${customerId} activada`);
      break;
    }

    // Nueva suscripcion iniciada
    case 'customer.subscription.created': {
      const sub = event.data.object;

      await supabase.from('subscriptions').insert({
        stripe_subscription_id: sub.id,
        stripe_customer_id: sub.customer,
        price_id:  sub.items.data[0].price.id,
        status:    sub.status,
        current_period_end: new Date(sub.current_period_end * 1000).toISOString(),
      });

      console.log(`[stripe] customer.subscription.created → sub ${sub.id}`);
      break;
    }

    // Suscripcion cancelada → revocar acceso
    case 'customer.subscription.deleted': {
      const sub = event.data.object;

      await supabase
        .from('accounts')
        .update({ status: 'canceled', canceled_at: new Date().toISOString() })
        .eq('stripe_customer_id', sub.customer);

      await supabase
        .from('subscriptions')
        .update({ status: 'canceled' })
        .eq('stripe_subscription_id', sub.id);

      await enqueueEmail({ type: 'cancellation', customerId: sub.customer });

      console.log(`[stripe] customer.subscription.deleted → cuenta ${sub.customer} desactivada`);
      break;
    }

    // Factura pagada → renovar acceso (renovacion mensual/anual)
    case 'invoice.paid': {
      const invoice = event.data.object;

      await supabase
        .from('accounts')
        .update({ status: 'active', last_payment_at: new Date().toISOString() })
        .eq('stripe_customer_id', invoice.customer);

      await supabase.from('payments').insert({
        stripe_invoice_id:  invoice.id,
        stripe_customer_id: invoice.customer,
        amount_eur: invoice.amount_paid / 100,
        currency:   invoice.currency.toUpperCase(),
        paid_at:    new Date(invoice.status_transitions.paid_at * 1000).toISOString(),
      });

      console.log(`[stripe] invoice.paid → ${invoice.amount_paid / 100} EUR (cliente ${invoice.customer})`);
      break;
    }

    // Pago fallido → notificar y marcar
    case 'payment_intent.payment_failed': {
      const pi = event.data.object;

      await supabase
        .from('accounts')
        .update({ payment_failed: true, payment_failed_at: new Date().toISOString() })
        .eq('stripe_customer_id', pi.customer);

      await enqueueEmail({
        type: 'payment_failed',
        customerId:   pi.customer,
        errorCode:    pi.last_payment_error?.code,
        errorMessage: pi.last_payment_error?.message,
      });

      console.log(`[stripe] payment_intent.payment_failed → cliente ${pi.customer}: ${pi.last_payment_error?.message}`);
      break;
    }

    default:
      console.log(`[stripe] Evento no manejado: ${event.type}`);
  }
} catch (handlerErr) {
  // Loguear pero responder 200: Stripe NO debe reintentar por errores internos
  console.error(`[stripe] Error en handler de ${event.type}:`, handlerErr);
  // En produccion: enviar a Sentry / Dead Letter Queue
}

// 4. RESPUESTA OK ────────────────────────
return res.status(200).json({ received: true });

} );

// ───────────────────────────────────────────── // Helpers // ─────────────────────────────────────────────

/**

  • Resuelve el nombre del plan a partir del Stripe Price ID.
  • Mantener sincronizado con la tabla de precios en Stripe Dashboard. */ function resolvePlan(priceId) { const MAP = { 'price_starter_monthly': 'starter', 'price_growth_monthly': 'growth', 'price_agency_monthly': 'agency', 'price_addon_ia': 'addon_ia', }; return MAP[priceId] ?? 'unknown'; }

/**

  • Encola un email transaccional.
  • Sustituir por llamada directa a Resend o sistema de colas (BullMQ, etc.) */ async function enqueueEmail(payload) { // TODO: conectar con Resend / cola interna console.log('[email-queue] enqueued:', JSON.stringify(payload)); }

module.exports = router;

// ───────────────────────────────────────────── // Registro de eventos de Stripe para CultivaSaaS // ───────────────────────────────────────────── // // Evento | Accion en BD | Email // ------------------------------------|-------------------------------|------------------------ // checkout.session.completed | upsert accounts (active) | Bienvenida + plan // customer.subscription.created | insert subscriptions | — // customer.subscription.deleted | update accounts (canceled) | Cancelacion confirmada // invoice.paid | update accounts (active) | — // payment_intent.payment_failed | update accounts (failed) | Alerta de pago fallido // // Variables de entorno necesarias: // STRIPE_SECRET_KEY sk_live_xxxxx // STRIPE_WEBHOOK_SECRET whsec_xxxxx (del endpoint en Stripe Dashboard) // SUPABASE_URL https://xxxx.supabase.co // SUPABASE_SERVICE_ROLE_KEY eyJxxx... // // Desarrollo local (sin cuenta Hookdeck): // npx hookdeck-cli listen 3000 stripe --path /webhooks/stripe

// qué_hace

Proporciona patrones de codigo y referencia para recibir, verificar y manejar eventos webhook de Stripe (pagos, suscripciones, facturas).

// cómo_lo_hace

Usa el SDK oficial de Stripe para verificar la firma HMAC-SHA256 del header Stripe-Signature sobre el cuerpo HTTP sin parsear, con ejemplos en Node.js y Python.

// ejemplo_de_uso

Indispensable al integrar Stripe en tu plataforma para reaccionar a eventos de pago sin procesar duplicados ni perder transacciones. Ej.: implementas en Node.js el handler de checkout.session.completed con verificación de firma en 20 minutos usando el snippet del skill.

// plataformas

Node.jsPythonExpressNext.jsFastAPIStripe
Categoría
Web
Tipo
Referencia
Nivel
Intermedio
Licencia
MIT
Seguridad
seguro · riesgo bajo
Versión
1.0.0

// opiniones_de_la_comunidad

Opiniones

Cargando opiniones…

// pase_cultiva_ia

Llévate todo el arsenal con el Pase

Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.

Pago único · IVA incluido · pago seguro con Stripe.

Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.