Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

Cobra una vez con Stripe Checkout

Recibe un pago único por algo que no es ni un plan ni un complemento, y confírmalo antes de entregar.

Un cobro único es una sesión de Stripe Checkout en modo pago: una línea, un importe, pagado una vez, sin crear suscripción. Úsalo cuando vendes algo que no es recurrente ni es una capacidad de la cuenta: un reporte, un boleto, un curso, una licencia de por vida que después otorgas como complemento. Esta guía es honesta con el punto de partida: el kit no trae ningún flujo de cobro único, ni clase, ni ruta, ni tabla. Lo que trae es todo aquello sobre lo que el flujo se apoya, y esta guía muestra cómo construirlo en pocas líneas.

Cuándo usarlo

Vendes Usa Por qué
Acceso que se renueva cada mes o año Un plan Stripe administra la suscripción, las facturas y los reintentos. Mira Crea y cotiza un plan.
Una capacidad de la cuenta, recurrente o gratis Un complemento El catálogo, el botón y la verificación de acceso ya existen. Mira Vende complementos.
Algo pagado una vez, entregado una vez Un cobro único Nada que renovar, nada que cancelar.
Una capacidad pagada una vez y conservada para siempre Un cobro único que otorga un complemento lifetime El pago es esta guía; el acceso es el resolutor de complementos.

Lo que el kit te da

Pieza Dónde Qué hace por el cobro
Llaves de Stripe STRIPE_KEY, STRIPE_SECRET, STRIPE_WEBHOOK_SECRET en .env, leídas por config/services.php El mismo secreto que usan los planes. Ninguna variable extra.
El cliente de Cashier WeblaborMx\BillingCore\Models\BillingAccount, uno por usuario, con stripe_id Todos los métodos de cobro de Cashier están en él, incluido checkoutCharge().
El cliente de Stripe Stripe\StripeClient, un singleton enlazado con tu secreto Para recuperar una sesión o un payment intent desde tu propio código.
El endpoint del webhook POST /api/stripe/webhook en routes/api.php Verifica la firma de Stripe con STRIPE_WEBHOOK_SECRET, registra cada evento y lo entrega al controlador del paquete.
La bitácora de webhooks Eventos de Webhook en el panel de administración, /admin/webhook-events Cada evento recibido, su payload, su estado de respuesta y cualquier error.
El evento WebhookReceived de Cashier Laravel\Cashier\Events\WebhookReceived Se despacha por cada evento antes de que el controlador busque un manejador. Tu listener va aquí.

El controlador del paquete maneja eventos de suscripciones y facturas. No tiene manejador para checkout.session.completed: el evento se responde con 200, se registra como éxito y no pasa nada más. Confirmar el pago es trabajo de tu listener.

Lanza el cobro desde código

Obtén la cuenta de facturación del usuario y abre la sesión. En un componente Livewire:

use WeblaborMx\BillingCore\BillingManager;

public function buyReport()
{
    $account = app(BillingManager::class)->context(auth()->user(), 'user')->account;

    $checkout = $account->checkoutCharge(
        49900,                       // importe en unidades menores: $499.00
        __('Annual report'),         // nombre del producto en la página de Stripe
        1,                           // cantidad
        [
            'success_url' => route('app.dashboard') . '?checkout=success',
            'cancel_url'  => route('app.dashboard') . '?checkout=cancel',
            'metadata'    => [
                'purpose' => 'annual_report',
                'user_id' => auth()->id(),
            ],
        ],
    );

    return redirect()->away($checkout->url);
}
Parámetro Qué es
$amount Entero, en la unidad más pequeña de la moneda. 49900 es 499.00 en una moneda de dos decimales.
$name La descripción de la línea en la página de Checkout.
$quantity 1 por omisión.
$sessionOptions Se pasan a checkout.sessions.create de Stripe. Define success_url, cancel_url y metadata. mode ya es payment.
$customerOptions Se pasan cuando hay que crear el cliente en Stripe. Normalmente vacío.
$productData Campos extra de product_data, como description o images.

checkoutCharge() crea el cliente de Stripe cuando la cuenta no tiene uno, así que el pago queda ligado al mismo cliente que usaría una suscripción posterior. El objeto devuelto expone los campos de la sesión: $checkout->url para redirigir, $checkout->id para guardarlo antes de redirigir, o return $checkout; desde un controlador para redirigir con un 303.

Moneda

checkoutCharge() cobra en config('cashier.currency'), que lee CASHIER_CURRENCY y vale usd por omisión. El kit no publica config/cashier.php y .env.example no lista la variable, así que un proyecto que cobra en pesos escribe:

CASHIER_CURRENCY=mxn

Cashier aplica esa moneda a todo cobro único. Los planes y los complementos no se ven afectados: su moneda es la de cada precio que creas en el panel de administración.

El importe es un entero en la unidad más pequeña, así que en monedas de dos decimales multiplicas por 100. Las monedas sin decimales toman el importe entero tal cual: JPY, KRW, VND, CLP, BIF, DJF, GNF, KMF, MGA, PYG, RWF, UGX, VUV, XAF, XOF, XPF. Pasar ¥500 como 50000 cobra quinientas veces de más, así que convierte por moneda antes de llamar.

A dónde regresa el cliente

Stripe manda el navegador a success_url cuando el formulario de pago termina y a cancel_url cuando el cliente se retira. Los checkouts propios del kit agregan ?checkout=success y ?checkout=cancel a la página de planes o al dashboard, y nada en el kit lee esos parámetros: están ahí para que tu página muestre un mensaje. Sigue la misma convención para que un solo componente reaccione a cada regreso.

Llegar a success_url no es una confirmación. Significa que la página de Stripe terminó; no prueba que el pago fue capturado, y algunos métodos de pago se completan de forma asíncrona. Entrega con el webhook, nunca con la redirección.

Confírmalo por webhook

Stripe manda checkout.session.completed cuando la sesión termina. Su data.object es la sesión: id, customer, payment_status, amount_total, currency, payment_intent y los metadata que definiste. Registra un listener en App\Providers\AppServiceProvider::boot():

use Laravel\Cashier\Events\WebhookReceived;

Event::listen(WebhookReceived::class, FulfilOneOffCharge::class);
namespace App\Listeners;

use Laravel\Cashier\Events\WebhookReceived;
use WeblaborMx\BillingCore\Models\BillingAccount;

class FulfilOneOffCharge
{
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] !== 'checkout.session.completed') {
            return;
        }

        $session = $event->payload['data']['object'];
        if ($session['mode'] !== 'payment' || $session['payment_status'] !== 'paid') {
            return;
        }

        $account = BillingAccount::where('stripe_id', $session['customer'])->first();
        if (! $account) {
            return;
        }

        // Entrega. Este ejemplo otorga una capacidad de por vida, con el id de la
        // sesión como referencia para que un evento repetido no otorgue nada dos veces.
        $account->grantAddOn('white_label', 'lifetime', ['reference' => $session['id']]);
    }
}

Qué tener presente:

  • El endpoint es /api/stripe/webhook. Agrégalo en el dashboard de Stripe con el evento checkout.session.completed seleccionado: Stripe sólo manda los eventos que marcas.
  • La firma se verifica antes de que corra tu listener, con STRIPE_WEBHOOK_SECRET.
  • Stripe reintenta un evento que no recibió un 2xx y puede entregar uno dos veces. Haz la entrega idempotente: guarda el id de la sesión y rechaza una segunda entrega.
  • payment_status es paid en pagos con tarjeta y unpaid en métodos diferidos; para esos Stripe manda después checkout.session.async_payment_succeeded, que tu listener puede tratar igual.
  • El kit no crea ningún registro del cobro. Si necesitas un historial de compras, crea tu propia tabla y escríbela desde este listener; la bitácora de Eventos de Webhook conserva el payload crudo pero no es un libro mayor.

Lee el resultado en tu código

Cuando el cliente regrese con un id de sesión que guardaste, pregúntale a Stripe en vez de a la redirección:

use Stripe\StripeClient;

$session = app(StripeClient::class)->checkout->sessions->retrieve($sessionId);

$session->payment_status;   // 'paid', 'unpaid' o 'no_payment_required'
$session->amount_total;     // unidades menores
$session->currency;         // 'mxn'
$session->metadata->purpose;

Úsalo para mostrar "pago recibido" en la página de regreso. Entrega sólo desde el webhook.

Pruébalo en local

Usa llaves de prueba en .env y reenvía los eventos de Stripe a tu máquina con la Stripe CLI:

stripe listen --forward-to your-project.test/api/stripe/webhook

El comando imprime un secreto de firma. Ponlo en .env como STRIPE_WEBHOOK_SECRET y limpia la caché de configuración. Luego corre el flujo con una tarjeta de prueba, 4242 4242 4242 4242, cualquier fecha futura y cualquier CVC. Cada evento aparece en Eventos de Webhook con su estado; un listener que lanza una excepción aparece ahí como fallido, con la excepción.

Para repetir el evento sin pagar otra vez:

stripe trigger checkout.session.completed

La sesión disparada pertenece a un cliente de prueba de Stripe, así que tu listener no encontrará una cuenta de facturación para ella; úsala para ver al endpoint responder 200 y a la bitácora llenarse, y usa un pago de prueba real para ver correr la entrega.