Skip to main content
lunarphp/stripe integrates Lunar with Stripe’s Payment Intents API. It supports automatic and manual capture policies, webhooks, address synchronization, refunds, and payment checks (AVS, postal code, CVC). Stripe is the reference implementation for a first-party Lunar payment driver: every other gateway except PayPal is community-maintained.

Installation

Require the Composer package

The service provider registers automatically.

Publish the configuration

This publishes config/lunar/stripe.php.

Publish the views (optional)

The addon ships a Blade/Livewire payment form. To customize it, publish the views.

Run migrations

The addon adds a stripe_payment_intents table.

Enable the driver

Register stripe as a driver for a payment type in config/lunar/payments.php.

Add Stripe credentials

The API key and webhook secret are read from config/services.php, not config/lunar/stripe.php.
Keys can be found in the Stripe Dashboard.

Configuration

The following options are available in config/lunar/stripe.php.

Backend usage

Create a PaymentIntent

Creates a Stripe PaymentIntent from a cart and stores a Lunar\Stripe\Models\StripePaymentIntent row linking it to the cart. If an intent already exists for the cart, the existing one is returned instead. The following parameters are sent by default, merged with anything passed in $options:

Amount conversion

Lunar stores prices as integers scaled by the currency’s configured decimal_places, which does not have to match the sub-unit Stripe expects for that currency code. Lunar\Stripe\Managers\StripeManager::toStripeAmount() converts the stored value back to the major unit using decimal_places, then re-scales it to whatever sub-unit Stripe requires. StripeManager::fromStripeAmount() performs the inverse conversion for amounts read back from Stripe, such as a charge or a refund. The sub-unit is chosen from the currency code, following Stripe’s currency reference: Conversion is done with integer arithmetic throughout, because float division misrounds at half-unit boundaries.

Fetch or create a PaymentIntent

Fetches the existing PaymentIntent for a cart if one exists, or creates a new one.

Retrieve the PaymentIntent ID from a cart

This reads from $cart->meta['payment_intent'] first, falling back to the cart’s active StripePaymentIntent record.

Fetch an existing PaymentIntent

Sync an existing PaymentIntent

If a PaymentIntent has been created and the cart contents change, call syncIntent to recalculate the cart and push the new total to Stripe.

Update an existing PaymentIntent

To update specific properties on the PaymentIntent without recalculating the cart, use updateIntent.
A PaymentIntent can also be updated directly by ID.

Update the shipping address

Syncs the cart’s shipping address onto the PaymentIntent without manually specifying every field.

Cancel a PaymentIntent

Available cancellation reasons:

Retrieve a payment method

Charges

Retrieve a specific charge

Get all charges for a PaymentIntent

Authorizing a payment

Lunar\Stripe\StripePaymentType::authorize() is the driver’s entry point, resolved through the Lunar\Core\Facades\Payments facade.
authorize():
  1. Rejects the attempt if a StripePaymentIntent record already exists for the intent and is no longer active (canceled or succeeded), or if the order is already placed.
  2. Retrieves the intent from Stripe and verifies its amount and currency match the order or cart total (see below).
  3. Captures the intent immediately when its status is requires_capture and the policy is automatic.
  4. Creates the order from the cart if one does not already exist.
  5. Stores the intent’s charges as transactions and places the order once the intent has succeeded.
Returns a Lunar\Core\DataObjects\PaymentAuthorize with success, message, orderId, and paymentType.

Amount and currency verification

Before capturing anything, authorize() checks the retrieved PaymentIntent against the expected total — the order’s total when authorizing against a placed/draft order, or the calculated cart’s total otherwise. The intent’s amount is compared at Stripe’s sub-unit scale via StripeManager::toStripeAmount(), and the currency is compared case-insensitively. A mismatch on either amount or currency fails the authorization before anything is captured, unless allow_partial_payment is enabled. This guard exists specifically so a client-supplied PaymentIntent ID cannot be used to place a more expensive order against a cheaper payment.

Orphaned intents

If order creation fails after Stripe has already reported the PaymentIntent as succeeded (for example, the cart cannot create a second order), the driver dispatches Lunar\Stripe\Events\OrphanedPaymentIntentDetected with the intent ID, cart ID, and failure reason, so the captured payment can be reconciled manually.

Capture and refund

Under the manual capture policy, a PaymentIntent is authorized but not captured until later.

Capturing a payment

$amount is optional and in the order currency’s minor unit; when omitted, the full authorized amount is captured. Internally this calls Stripe’s capture endpoint, re-fetches the intent, and stores its charges as transactions via Lunar\Stripe\Actions\UpdateOrderFromIntent. The same call is available as a verb on the transaction itself:

Refunding a payment

$amount is required and in the order currency’s minor unit. The driver creates a Stripe refund against the charge’s PaymentIntent and records a refund transaction, populating Lunar\Core\DataObjects\PaymentRefund::$transaction with the row it created. Equivalently:
For a refund that also allocates against specific order lines — rather than a flat amount against a transaction — use Lunar\Core\Models\Order::refund() with a Lunar\Core\DataObjects\RefundRequest. It resolves the capture transaction, computes the amount from the requested lines, shipping, and adjustment, and dispatches to the driver’s refund() under the hood, so the amount-matching guard above still applies to what Stripe actually receives.

Payment checks

The driver surfaces the AVS line 1, AVS postal code, and CVC check results Stripe returns on a charge.

Webhooks

Register a webhook endpoint in the Stripe Dashboard. Follow the Stripe webhook guide to set this up.
The path is configurable via webhook_path.

Supported events

StripeWebhookMiddleware only lets the following event types through to the controller; every other event returns a 200 without further processing:
  • payment_intent.succeeded
  • payment_intent.payment_failed

Webhook signing secret

How a webhook is processed

The controller extracts the PaymentIntent ID and, if present, an order ID from the event’s metadata, then dispatches Lunar\Stripe\Jobs\ProcessStripeWebhook on a five-second delay (so it lands after a same-request authorize() call would have already processed the intent). The job re-authorizes against the order if one is known, or against the cart otherwise. If neither can be found, it dispatches Lunar\Stripe\Events\Webhook\CartMissingForIntent.

Extending event parameter resolution

The PaymentIntent ID and order ID extraction can be customized by overriding the ProcessesEventParameters binding.

CartMissingForIntent

Dispatched when a webhook is received for a PaymentIntent, but no matching cart or order can be found. It broadcasts on a private stripe-webhooks channel.

Manual order processing

If webhooks are disabled, or an order needs to be processed manually, authorize directly.

Livewire component

The addon includes a Livewire payment form component that handles PaymentIntent creation and Stripe Elements rendering.
Include the Stripe.js script in the page layout with the Blade directive.

Storefront example

API route for PaymentIntents

Stripe Elements

This example uses Stripe’s Payment Element. For more information, see the Stripe Elements guide.

Database

The addon adds a stripe_payment_intents table (Lunar\Stripe\Models\StripePaymentIntent) tracking PaymentIntents and their relationship to carts and orders.

Testing

The addon includes a mock HTTP client for testing without making real Stripe API calls.