Cómo integrar Stripe con Laravel Cashier + Vue Stripe

Video thumbnail

Integrar Stripe con Laravel Cashier puede parecer complejo al principio, pero en realidad es una de las formas más seguras y flexibles de gestionar pagos y suscripciones en tus proyectos Laravel.

Stripe nos permite realizar pagos en distintas plataformas, y para nuestro caso de interés, que es desarrollar aplicaciones con Laravel, podemos integrarlo directamente en nuestra aplicación. Así, los clientes podrán realizar pagos y comprar productos dentro de nuestra plataforma renderizando desde un componente de Blade/Template en Laravel un botón de Stripe para realizar la compra.

En este artículo te explico cómo hacerlo paso a paso, tal como lo implementé en mi propio entorno (Laravel con Vue 3).

Además, te contaré algunos errores que me encontré en producción y cómo los solucioné para que tú no tengas que pasar por lo mismo.

Laravel Cashier es la forma oficial que tenemos para integrar Stripe con Laravel, así de simple, permite emplear Stripe, fácilmente; ademas, veremos como emplear Vue Stripe junto con Laravel.

Crear cuenta en Stripe

Lo primero que debes hacer es crear una cuenta en Stripe:

https://stripe.com/es

Si ya tienes una cuenta, puedes iniciar sesión directamente; si no, sigue los pasos de registro que Stripe te indica. Una vez creada, tendrás acceso al dashboard.

Dashboard de Stripe

En el dashboard encontrarás todo lo que necesitamos para el curso:

  • Modo de prueba y producción: Puedes alternar entre ellos.
  • Claves públicas y secretas: Necesarias para integrar Stripe en tu proyecto.
  • Opciones de pago: Apple Pay, Google Pay, PayPal y más.
  • Catálogo de productos: Aquí puedes crear los productos que venderás en tu aplicación.

Es importante mantener las claves en modo prueba mientras desarrollamos y reemplazarlas con las de producción cuando lancemos la aplicación.

Qué ofrece Stripe

Stripe funciona de manera similar a PayPal, pero ofrece más opciones:

  • Pagos únicos
  • Pagos recurrentes
  • Suscripciones
  • Gestión de productos y precios

Para configurar los productos:

  • Ve a Catálogo de productos en tu dashboard.
  • Crea los productos que vas a vender (por ejemplo, “Zapatos” o “Libro Laravel”).
  • Define los precios para cada producto.

Puedes crear productos por categoría, marca o cualquier criterio que necesites.

Stripe y Laravel

Laravel ofrece una integración oficial con Stripe mediante el paquete Laravel Cashier:

Con Cashier podemos:

  • Crear clientes y productos
  • Gestionar pagos únicos y suscripciones
  • Generar facturas
  • Manejar pagos recurrentes

Además, Stripe también ofrece su API nativa que se puede usar en cualquier proyecto PHP:

Puedes elegir entre usar la API oficial de Stripe o Laravel Cashier, dependiendo de tus necesidades. Por ejemplo, para un pago simple, la API oficial puede ser suficiente; para pagos recurrentes o suscripciones, Cashier facilita mucho el trabajo.

Instalación y configuración de Laravel Cashier

Para instalar y configurar Cashier en Laravel, debes publicar las migraciones y el archivo de configuración:

$ php artisan vendor:publish --tag="cashier-migrations"
$ php artisan vendor:publish --tag="cashier-config"

Una vez hecho esto, podrás gestionar clientes, suscripciones y pagos directamente desde tu aplicación Laravel.

Alternativa con Vue

Si tu proyecto utiliza Laravel con Inertia o Vue, también puedes integrar Stripe mediante un plugin de Vue llamado Vue Stripe:

https://vuestripe.com/

Este plugin es muy útil cuando solo quieres vender productos rápidamente y gestionar pagos de manera sencilla.

Resumen

En resumen, Stripe es una billetera electrónica muy versátil que podemos emplear:

  • Directamente con su API nativa
  • A través de Laravel Cashier
  • Con plugins de Vue para aplicaciones front-end

¿Qué es Laravel Cashier y por qué usarlo con Stripe?

Laravel Cashier es un paquete oficial que simplifica todo el manejo de pagos recurrentes y suscripciones con Stripe (y Paddle). En lugar de escribir lógica personalizada, Cashier te ofrece métodos listos para crear, cancelar o verificar suscripciones.

En mi caso, probé integrar Stripe “a pelo” y luego con Cashier, y la diferencia es enorme. Cashier te evita lidiar con tokens, IDs y webhooks directamente; todo se resume en métodos como newSubscription() o subscribed().

Ventajas de usar Cashier

  • Implementación rápida sin escribir lógica de pagos desde cero.
  • Manejo automático de suscripciones, cancelaciones y períodos de gracia.
  • Compatibilidad nativa con Laravel.
  • Integración sencilla con Stripe Checkout.

Eso sí, si tu app es pequeña y solo quieres cobrar un producto puntual, usar Cashier podría ser demasiado. En esos casos, Stripe Checkout directo puede ser suficiente.

Antes de seguir avanzando tienes que venir aquí a la primera página que te presentaba cuando te indicaba al inicio de esta sección hace un par de videos atrás en las cuales te indicaba que cuando te autenticabas caías en esta página que es el dashboard de Stripe:

https://dashboard.stripe.com/

Entonces estamos aquí. Tienes que venir y activar el "Entorno de prueba" que sería el entorno para realizar las pruebas de desarrollo.

Crear entorno de Prueba

Como primer paso, debemos ir al siguiente enlace: https://dashboard.stripe.com/ e interactuar con el interruptor de modo de desarrollo en la esquina superior derecha.

Opción de crear entorno de prueba en Stripe

Y creamos formalmente el entorno de prueba para tus pruebas locales:

Crear entorno de prueba

Crear productos de prueba

A diferencia de PayPal, en Stripe debemos crear los productos que vamos a vender para poder referenciarlos en nuestra aplicación en Laravel/Vue mediante un identificador único; para ello accedemos desde el catálogo de productos:

https://dashboard.stripe.com/test/products?active=true

Creamos al menos un producto desde el botón de "Crea un producto":

Producto de ejemplo

Desde el detalle del producto, en el precio configurado, al darle un click, tendrás el identificador del precio (Price ID), que usaremos más adelante para poder hacer una compra a este producto con el precio seleccionado:

Copiar ID del precio

Puedes crear más tarifas/precios ya que un producto puede tener de 1 a n precios que son los que empleamos en nuestra aplicación para configurar los pagos; los pagos iniciales deben ser de tipo puntual NO los recurrentes, los pagos recurrentes son exclusivos para las suscripciones.

Stripe maneja una estructura un poco diferente a lo que estamos acostumbrados por ejemplo con PayPal en la cual simplemente establecemos un precio de forma dinámica en caliente y listo; aquí no podemos establecer el precio o el monto libremente en el código frontend, tenemos que pasar una referencia (ID) de una tarifa ya preconfigurada en nuestra API.

Crear Producto

Creamos al menos un producto desde el catálogo asignándole un nombre descriptivo.

Recurrentes vs Pago único

Al configurar los precios puedes indicar periodos de facturación (6 meses, 3 meses, anual, semanal, diario o personalizado) lo cual pertenece a suscripciones. Para facilitarnos la vida con una compra estándar, vamos a colocar aquí Puntual que significa que es un único pago y estableces el monto que desees (por ejemplo, 1 USD/EUR).

Products

Crear Precio

Damos clic en añadir producto y ahí lo tenemos. Ahora desde las propiedades del catálogo de precios copiamos el ID del precio generado que empieza por el prefijo price_....

⚙️ Instalación de Laravel Cashier y configuración de Stripe

Ejecutamos el comando Composer tradicional en la terminal de tu proyecto:

$ composer require laravel/cashier

Y publicamos su configuración y migraciones correspondientes del sistema:

$ php artisan vendor:publish --tag="cashier-config"
$ php artisan vendor:publish --tag="cashier-migrations"
$ php artisan migrate

Esto crea tres tablas principales en tu base de datos (customers, subscriptions, subscription_items) y el archivo de configuración global config/cashier.php.

Configurar las claves de Stripe

En tu archivo de entorno .env agrega las siguientes variables obtenidas del Dashboard:

STRIPE_KEY=tu-clave-publica
STRIPE_SECRET=tu-clave-secreta
STRIPE_WEBHOOK_SECRET=tu-webhook-secret

Error común en producción: “Missing API key provided.”
Si te pasa, se debe a que no has publicado o limpiado la configuración correctamente en caché. Asegúrate de correr php artisan vendor:publish --tag="cashier-config" antes de desplegar.

Implementar pagos únicos con Stripe Checkout

Stripe Checkout te permite redirigir al usuario a una página segura administrada por Stripe para pagar, sin almacenar tarjetas o datos sensibles en tu servidor.

En tu controlador de Laravel:

<?php

namespace App\Http\Controllers;

use Laravel\Cashier\Checkout;
use Illuminate\Http\Request;

class StripeController extends Controller
{
    public function createSession(string $priceId)
    {
        $session = Checkout::guest()->create($priceId, [
            'mode' => 'payment',
            'success_url' => url('/success').'?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => url('/cancel'),
        ]);

        return $session->id;
    }
}

Este método crea una Checkout Session y devuelve su ID, que puedes usar desde el frontend. Si en vez de retornar el ID retornas el objeto completo de la sesión, Laravel automáticamente hace la redirección HTTP hacia la pasarela segura de Stripe.

Prefiero este enfoque porque redirige al usuario a Stripe (como hace PayPal). Así evitas manejar formularios de tarjetas dentro de tu app y generas más confianza.

Suscripciones recurrentes con Laravel Cashier

Para habilitar las suscripciones recurrentes:

  • Crea un Price ID recurrente en lugar de puntual desde el dashboard de Stripe.
  • Usa el método newSubscription() integrado en tu modelo de usuario en el backend:
use Illuminate\Http\Request;

Route::post('/user/subscribe', function (Request $request) {
   $request->user()->newSubscription('default', 'price_monthly')
       ->create($request->paymentMethodId);
});

Indicamos cuál sería el plan y el precio, y lo creamos mediante el token del método de pago enviado por el cliente. A partir de aquí podemos configurar suscripciones de prueba (trials) definiendo los días de gracia, o gestionar cupones.

Cupones y descuentos

En cuanto a los cupones y descuentos, puedes gestionarlos directamente desde la aplicación mediante tu propia lógica interna antes de enviar el precio modificado, o mapear los códigos de descuento nativos de Stripe Checkout.

Verificación de suscripciones

Para comprobar si el usuario autenticado cuenta con un plan de suscripción activo para restringir accesos o vistas de Blade:

if ($user->subscribed('default')) {
   // Acceso permitido
}

Ejemplo completo de suscripción en Laravel

use App\Models\User;

Route::get('/stripe/new-subscription', function () {
    $user = User::find(1);
    
    dd(
        $user->newSubscription('default', 'YOUR_RECURRENT_PRICE_ID')
             ->create('<USER_PAYMENT_METHOD_ID>')
    );
});

La etiqueta 'default' puede ser reemplazada por cualquier otro nombre que desees (ej. 'premium'), ya que funciona como un identificador de control interno.

Períodos de Gracia

Stripe maneja lo que se conoce como un periodo de gracia. Si el usuario cancela la suscripción pero aún le quedan días disponibles del mes que ya pagó, puede seguir disfrutando del servicio. Puedes validarlo con:

$user->subscription('default')->onGracePeriod();

Verificar si la suscripción sigue siendo recurrente

Para comprobar si la suscripción del usuario sigue activa y se renovará automáticamente al finalizar el ciclo:

$user->subscription('default')->recurring();

Integración de Vue Stripe 4 en Laravel - Legacy

Si utilizas Vue 3 en el desarrollo de tu frontend, puedes valerte del paquete oficial de la comunidad para integrar los componentes de pago interactivos:

$ npm install @vue-stripe/vue-stripe@^4.5.0

Un ejemplo de componente básico implementando Checkout:

<template>
 <div>
   <stripe-checkout
     ref="checkoutRef"
     mode="payment"
     :pk="publishableKey"
     :line-items="lineItems"
     :success-url="successURL"
     :cancel-url="cancelURL"
     @loading="v => loading = v"
   />
   <button :disabled="loading" @click="submit">Pay now!</button>
 </div>
</template>

<script>
import { StripeCheckout } from '@vue-stripe/vue-stripe';

export default {
 components: { StripeCheckout },
 data() {
   this.publishableKey = 'pk_test_...';
   return {
     loading: false,
     lineItems: [{ price: 'price_1234', quantity: 1 }],
     successURL: 'http://tuapp.test/success',
     cancelURL: 'http://tuapp.test/cancel',
   };
 },
 methods: {
   submit() {
     this.$refs.checkoutRef.redirectToCheckout();
   },
 },
};
</script>

Configuración de enrutamiento con vue-router de ejemplo:

import { createRouter, createWebHistory } from "vue-router";
import List from './componets/ListComponent.vue';
import OnePayment from "./componets/stripe/OnePayment.vue";

const routes = [
    {
        name: 'stripe',
        path: '/vue/stripe/one-payment',
        component: OnePayment
    },
    {
        name: 'success',
        path: '/vue/stripe/success',
        component: List
    },
    {
        name: 'cancel',
        path: '/vue/stripe/cancel',
        component: List
    },
];

const router = createRouter({
    history: createWebHistory(),
    routes: routes
});

export default router;

Integración solo en el cliente, riesgos y limitantes en el uso de Vue Stripe

Video thumbnail

Si utilizas únicamente la integración cliente sin llamadas asíncronas seguras al backend para inicializar las sesiones, verás la siguiente excepción de seguridad en tu consola web:

v3:1 Uncaught (in promise) IntegrationError: The Checkout client-only integration is not enabled. Enable it in the Dashboard at https://dashboard.stripe.com/account/checkout/settings.

️ Claves Públicas y Secretas: Seguridad en Transacciones

En las pasarelas bancarias modernas siempre debe existir un juego de llaves asimétricas. La clave secreta debe guardarse exclusivamente en tu archivo .env de Laravel y nunca exponerse al cliente, garantizando que el servidor valide la autenticidad e integridad del cobro.

⚠️ Riesgos de Seguridad al Usar Solo la Clave Pública

Si realizas una integración puramente desde el cliente (exponiendo toda la lógica en JavaScript), corres el riesgo de que un atacante altere los IDs de los precios o manipule la respuesta del cliente de forma maliciosa. Por ello, Stripe obliga a activar de forma explícita esta opción insegura en la configuración de la cuenta si no usas llamadas de servidor.

El Problema Principal: Lógica de Asignación de Productos

La mayor limitante radica en que si no utilizas un flujo controlado por tu backend en conjunto con Webhooks seguros de Stripe, tu aplicación Laravel nunca se enterará a nivel de base de datos cuándo se liberó la transacción de manera exitosa para activar el producto comprado (un curso, libro, etc.) al usuario correspondiente. Resolveremos y mitigaremos estos puntos ciegos en las siguientes secciones usando webhooks.

Integración con Vue, Forma moderna

Video thumbnail

Todo el código presentado antes queda igual, salvo el componente de Vue OnePayment.vue que queda de la siguiente manera:

resources/js/vue/componets/stripe/OnePayment.vue

<template>
  <div>
    <div v-if="publishableKey">
      <div v-if="checkoutUrl">
        <button @click="submit" :disabled="loading">Pay Now</button>
      </div>
      <p v-else>Cargando sesión...</p>
    </div>
    <div v-else>Cargando configuración de Stripe...</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      publishableKey: window.Laravel.clientStripe,
      checkoutUrl: '',
      lineItems: [
        {
          price: 'price_1QTO20EHJX14M8EEKW0SS0s7', // recurrente
          quantity: 1
        },
      ]
    };
  },
  methods: {
    submit() {
      if (this.checkoutUrl) {
        window.location.href = this.checkoutUrl;
      }
    }
  },
  async mounted() {
    try {
      const res = await this.$axios.get('/api/stripe/create-session/' + this.lineItems[0].price);
      console.log("Respuesta de Stripe:", res.data);

      // El objeto Session devuelto por Stripe (vía Laravel) contiene una propiedad 'url'.
      // Redirigir directamente a esta URL es el método actual recomendado.
      if (res.data && res.data.url) {
        this.checkoutUrl = res.data.url;
      } else {
        console.error("No se pudo obtener la URL de Checkout del servidor. Estructura recibida:", res.data);
      }
    } catch (error) {
      console.error("Error obteniendo sesión:", error);
    }
  }
}
</script>

Como puedes apreciar, NO es necesario el plugin de Vue Stripe, simplemente procesamos de manera manual la redirección nativa desde el objeto Checkout.

app/Http/Controllers/Api/StripeController.php

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;

class StripeController extends Controller
{
    public function createSession()
    {
        // ... lógica de inicialización de la sesión
        
        return response()->json([
            'id' => $session->id,
            'url' => $session->url
        ]);
    }
}

Hacemos las redirecciones de manera manual sin depender de librerías de terceros en el cliente.

No usar el priceID para procesar los pagos

También podemos suministrar el monto directamente desde el cliente dinámicamente si no deseamos atarnos a un catálogo estático:

const res = await this.$axios.get(this.$api.stripeCreateSessionId, {
  params: {
    amount: this.basePrice,
  }
});

Y en el servidor administramos el flujo condicional para estructurar los datos del producto al vuelo:

public function createSessionId()
{
    $successRouteUrl = request('successRouteUrl');
    $checkoutSessionId = '{CHECKOUT_SESSION_ID}';

    $separator = strpos($successRouteUrl, '?') === false ? '?' : '&';
    $url = "{$successRouteUrl}{$separator}session_id={$checkoutSessionId}";

    if (request()->has('amount')) {
        $sessionId = $this->createSessionByAmount(request('amount'), $url);
    } else {
        $sessionId = $this->createSessionByPriceId(request('price_id'), $url);
    }

    return $this->successResponse($sessionId);
}

public function createSessionByPriceId(string $priceId, string $successRouteUrl, string $cancelUrl = "https://academy.desarrollolibre.net/")
{
    $session = Checkout::guest()->create(
        $priceId,
        [
            'mode' => 'payment',
            'success_url' => $successRouteUrl,
            'cancel_url' => $cancelUrl,
        ],
    );

    return response()->json([
        'id' => $session->id,
        'url' => $session->url
    ]);
}

public function createSessionByAmount(float $amount, string $successRouteUrl, string $cancelUrl = "https://academy.desarrollolibre.net/")
{
    $session = Cashier::stripe()->checkout->sessions->create([
        'success_url' => $successRouteUrl,
        'cancel_url' => $cancelUrl,
        'line_items' => [
            [
                'price_data' => [
                    'currency' => 'usd',
                    'product_data' => [
                        'name' => 'Compra en Desarrollolibre Academy',
                    ],
                    'unit_amount' => $amount * 100, // Stripe maneja centavos
                ],
                'quantity' => 1,
            ],
        ],
        'mode' => 'payment',
    ]);

    return $session->id;
}

Extra: Cómo habilitar el Instant Payment Notification (IPN) en PayPal

Te comento cómo crear el Instant Payment Notification (IPN) en PayPal; el IPN es una notificación asíncrona de pago realizada por PayPal de forma directa a una URL que nosotros configuremos en su plataforma.

Al contrario de cómo funcionaba hace un tiempo, en la cual era obligatorio contar con un certificado SSL estrictamente firmado en nuestro servidor local para poder guardar y enlazar la URL de pruebas del IPN, hoy en día esto ya no es necesario. Cualquier URL con un dominio público (o expuesta a Internet mediante herramientas de túnel como Ngrok) servirá perfectamente para recibir la notificación de pago en tus entornos locales de desarrollo.

El vídeo correspondiente lo tienes disponible aquí: https://www.youtube.com/embed/4IXCmMV5zPA

Pasos para su configuración

  1. Primero iniciamos sesión con nuestra cuenta de desarrollo (Sandbox) o producción en el panel de PayPal.
  2. Ve a las opciones de tu perfil (Profile Settings).
  3. Selecciona el apartado de herramientas de venta (My selling tools).
  4. Ubica y haz clic en la opción Instant payment notifications.
  5. Establece la URL pública de tu webhook. Esta ruta lógicamente estará mapeada a un controlador intermedio en tu backend (como un controlador de Laravel) para interceptar los parámetros post-pago, procesar la verificación y liberar los accesos al usuario.

Siguiente paso, aprende a dar los primeros pasos con Boost con este tutorial sobre cómo emplear la SDK de Laravel.

Aprende paso a paso cómo crear un price recurrente en Stripe y vincularlo con tu aplicación Laravel usando Cashier. Descubre cómo gestionar suscripciones, verificar periodos de gracia, y permitir que los usuarios cancelen o reactiven su plan fácilmente.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english