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.