Manejar Reembolsos y Disputas de Facturas

Video
30 min~5 min lectura

Reproductor de video

Concepto clave

Los reembolsos y disputas son dos mecanismos de protección para clientes que afectan directamente tus ingresos y reputación. Imagina que gestionas un gimnasio: un reembolso es cuando un miembro pide devolución de su cuota mensual porque no pudo asistir, mientras que una disputa es cuando ese miembro contacta a su banco para revertir el cargo alegando fraude. En Stripe, los reembolsos los inicias tú como negocio, mientras que las disputas las inicia el cliente a través de su banco.

La diferencia crítica está en las consecuencias: un reembolso es un acuerdo directo con el cliente, pero una disputa implica costos adicionales (tarifas de $15-25) y puede afectar tu tasa de disputas. Si superas el 0.75% de transacciones disputadas, Stripe puede penalizarte. Piensa en esto como la diferencia entre resolver un problema con un cliente amablemente versus que ese cliente llame a su abogado.

Cómo funciona en la práctica

Cuando un cliente solicita un reembolso, el flujo es: 1) Recibes la solicitud por correo o tu sistema, 2) Verificas la factura y el pago en Stripe Dashboard o API, 3) Decides si aprobar el reembolso parcial o total, 4) Lo procesas y notificas al cliente. Para disputas, el flujo es diferente: 1) Stripe te notifica via webhook, 2) Tienes 7-21 días para presentar evidencia, 3) Reúnes documentos como confirmaciones de envío o registros de acceso, 4) Envías la evidencia y esperas la decisión del banco.

Ejemplo paso a paso para un reembolso: María compra una suscripción anual a tu plataforma de cursos por $120. A los 3 meses pide cancelación y reembolso parcial. Primero, cancelas la suscripción para evitar futuros cargos. Luego, calculas el reembolso proporcional: $120 / 12 meses = $10 por mes, 9 meses restantes = $90. Finalmente, procesas el reembolso de $90 y envías un recibo actualizado.

Codigo en accion

Procesar un reembolso completo via API:

// Antes: Solo cancelar la suscripción
async function cancelSubscription(subscriptionId) {
  const subscription = await stripe.subscriptions.update(subscriptionId, {
    cancel_at_period_end: true
  });
  return subscription;
}

// Después: Cancelar Y procesar reembolso
async function handleRefundRequest(paymentIntentId, reason) {
  // 1. Crear el reembolso
  const refund = await stripe.refunds.create({
    payment_intent: paymentIntentId,
    reason: reason // 'requested_by_customer' o 'duplicate'
  });
  
  // 2. Actualizar tu base de datos
  await db.update('invoices', {
    status: 'refunded',
    refund_id: refund.id
  }, { payment_intent_id: paymentIntentId });
  
  // 3. Notificar al cliente
  await sendEmail(refund.receipt_email, {
    subject: 'Reembolso procesado',
    amount: refund.amount / 100
  });
  
  return refund;
}

Manejar una disputa via webhook:

# Configuración del endpoint webhook
@app.route('/webhook/stripe', methods=['POST'])
def stripe_webhook():
    payload = request.get_data(as_text=True)
    sig_header = request.headers.get('Stripe-Signature')
    
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, webhook_secret
        )
    except ValueError:
        return 'Invalid payload', 400
    except stripe.error.SignatureVerificationError:
        return 'Invalid signature', 400
    
    # Manejar evento de disputa
    if event['type'] == 'charge.dispute.created':
        dispute = event['data']['object']
        
        # 1. Registrar en tu sistema
        db.insert('disputes', {
            'dispute_id': dispute['id'],
            'charge_id': dispute['charge'],
            'amount': dispute['amount'],
            'status': 'needs_response',
            'due_by': dispute['evidence_details']['due_by']
        })
        
        # 2. Iniciar proceso de recolección de evidencia
        start_evidence_collection(dispute['id'])
        
        return 'Dispute logged', 200
    
    return 'Event received', 200

Errores comunes

  • Reembolsar sin verificar el estado del pago: Intentar reembolsar un pago que ya fue reembolsado o está en disputa causa errores. Siempre verifica payment_intent.status antes de proceder.
  • Ignorar los plazos de disputas: Cada banco tiene diferentes ventanas (7, 14 o 21 días). Si no envías evidencia a tiempo, pierdes automáticamente.
  • No documentar la comunicación con el cliente: En disputas, los correos y chats son evidencia crucial. Guarda todo en tu sistema, no solo en tu bandeja de entrada.
  • Reembolsar el monto incorrecto: Para suscripciones, calcula proporcionalmente basado en el uso. Usa subscription.current_period_start y end para determinar el periodo no utilizado.
  • No actualizar el estado en tu base de datos: Después de un reembolso o disputa, actualiza los registros locales para evitar duplicados o estados inconsistentes.

Checklist de dominio

  1. Puedo crear un reembolso parcial o total via API con manejo de errores
  2. Sé configurar y verificar webhooks para eventos de disputas
  3. He implementado un sistema para recolectar y enviar evidencia dentro de los plazos
  4. Entiendo la diferencia entre reason en reembolsos y cómo afecta las métricas
  5. Puedo calcular reembolsos proporcionales para suscripciones basado en el periodo
  6. He integrado notificaciones automáticas a clientes después de reembolsos
  7. Sé monitorear mi tasa de disputas en Stripe Dashboard y tomar acciones preventivas

Implementar sistema de reembolsos automáticos con límites

Construye un endpoint que procese reembolsos automáticos con las siguientes reglas:

  1. Crea una ruta POST /api/refunds que acepte { payment_intent_id, reason, amount }
  2. Valida que el reembolso no exceda el 50% del monto original para montos mayores a $100
  3. Si es una suscripción, calcula el monto proporcional basado en días no usados
  4. Registra cada reembolso en tu base de datos con timestamp y usuario responsable
  5. Envía un email de confirmación con el recibo de Stripe adjunto
  6. Maneja al menos 3 casos de error específicos (pago ya reembolsado, monto inválido, etc.)

Usa el lenguaje de backend de tu preferencia y prueba con datos reales del Sandbox de Stripe.

Pistas
  • Usa el método payment_intents.retrieve() para obtener el monto original antes de validar
  • Para suscripciones, necesitarás buscar la invoice asociada al payment intent primero
  • Considera usar una cola de trabajos para el envío de emails así no bloqueas la respuesta HTTP

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.

De lección a portfolio

Convertí esta lección en una prueba técnica visible.

Una app pequeña publicada, con README y decisiones explicadas, funciona mejor que una lista de tecnologías sueltas.

Paso 1

Creá una demo mínima que use el concepto de la lección.

Paso 2

Escribí un README corto con objetivo, stack, decisión técnica y mejora futura.

Paso 3

Publicá la demo y enlazala desde tu perfil profesional.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión