This guide walks through wiring a payment provider into Lunar end to end, using Stripe as the worked example.Documentation Index
Fetch the complete documentation index at: https://docs.lunarphp.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Lunar uses a driver-based payment system through thePayments facade. This guide focuses on integrating Stripe as the payment provider, covering the complete flow from creating a payment intent to handling webhooks. The same high-level patterns apply to other payment drivers.
The examples below use standard Laravel controllers and Blade templates. The same concepts apply whether the storefront is built with Livewire, Inertia, or a headless API.
Payment Architecture
Lunar’s payment system has three layers:- Payment Manager (
Lunar\Facades\Payments) — routes payment calls to the correct driver based on configuration - Payment Type (e.g.,
Lunar\Stripe\StripePaymentType) — handles authorization, capture, and refund logic for a specific provider - Transactions (
Lunar\Models\Transaction) — records of every payment event stored against the order
config/lunar/payments.php:
Payments::driver()) to a driver class and the order statuses to assign on success.
Setting Up Stripe
Installation
Configuration
Publish the configuration file:.env file:
Stripe Configuration Options
The published config file (config/lunar/stripe.php) contains:
| Option | Description |
|---|---|
webhook_path | The URL path where Stripe sends webhook events |
policy | automatic captures payment immediately; manual authorizes first and captures later |
sync_addresses | When true, billing and shipping addresses from Stripe are synced to the order |
status_mapping | Maps Stripe PaymentIntent statuses to Lunar order statuses |
Register the Payment Driver
In a service provider (e.g.,AppServiceProvider), register the Stripe driver:
Payment Flow
The Stripe integration follows this lifecycle:- Create a PaymentIntent — when the customer reaches checkout, a Stripe PaymentIntent is created for the cart total
- Collect payment details — the frontend renders Stripe’s Payment Element to collect card details
- Confirm the payment — Stripe.js confirms the payment on the client side and redirects back
- Authorize in Lunar — the return handler calls
Payments::authorize()to complete the order - Webhook backup — Stripe sends webhook events to handle edge cases (browser closed, network issues)
Creating a Payment Intent
Use theStripe facade to create or fetch a PaymentIntent for the cart. The intent amount is automatically calculated from the cart total.
fetchOrCreateIntent() checks whether an active intent already exists for the cart. If one exists, it syncs the amount and returns it. If none exists, it creates a new one.
The PaymentIntent is linked to the cart through the
lunar_stripe_payment_intents table. This table tracks the intent ID, cart ID, status, and processing state.Syncing the Intent
If the cart total changes (for example, after applying a coupon or changing the shipping method), sync the intent to update the amount:Frontend Integration
Stripe’s Payment Element renders a prebuilt UI for collecting payment details. It supports cards, wallets, bank transfers, and other payment methods automatically.Including Stripe.js
Add the Stripe.js script to the checkout layout:Rendering the Payment Element
Setting
billingDetails: 'never' on the Payment Element prevents Stripe from showing its own billing address fields, since the address is already collected during checkout and passed in confirmParams.What Happens After Confirmation
AfterconfirmPayment() succeeds, Stripe redirects the customer to the return_url with query parameters including payment_intent and payment_intent_client_secret. The callback handler uses these to authorize the payment in Lunar.
Handling the Payment Callback
When Stripe redirects back to the storefront, authorize the payment through thePayments facade:
authorize() method returns a PaymentAuthorize object:
| Property | Type | Description |
|---|---|---|
success | bool | Whether the payment was authorized |
message | ?string | Error or status message |
orderId | ?int | The ID of the placed order |
paymentType | ?string | The payment driver used |
What Happens During Authorization
Whenauthorize() is called on the Stripe payment type:
- Retrieves the
StripePaymentIntentrecord linked to the cart - Fetches the current status from Stripe’s API
- Creates an order from the cart (if one does not already exist)
- If the policy is
automaticand the status isrequires_capture, captures the payment immediately - Updates the order status based on Stripe’s status mapping
- Sets
placed_aton the order when the payment succeeds - Stores charge details (card brand, last four digits) as transaction records
Webhooks
Stripe sends webhook events for payment status changes. These handle edge cases where the customer’s browser closes before the callback completes, or when 3D Secure authentication completes asynchronously.Webhook Route
The Stripe package registers a webhook route automatically at the path configured inconfig/lunar/stripe.php (default: stripe/webhook).
Configure the webhook endpoint in the Stripe dashboard:
.env:
How Webhooks Are Processed
When a webhook event arrives:- The signature is verified against the webhook secret
- A
ProcessStripeWebhookjob is dispatched (with a short delay to allow the callback to complete first) - The job fetches the PaymentIntent from Stripe and updates the order status accordingly
Capture and Refund
Manual Capture
When using themanual capture policy, payments are authorized but not captured immediately. This is useful for stores that want to review orders before charging the customer.
$amount parameter is in the currency’s smallest unit (e.g., cents). Pass 0 to capture the full amount.
Refunds
Issue a full or partial refund through the payment driver:refund is created automatically.
Transactions
Every payment event is recorded as aLunar\Models\Transaction on the order. Transactions provide a complete audit trail.
| Field | Type | Description |
|---|---|---|
id | bigint | Primary key |
parent_transaction_id | foreignId nullable | The ID of the preceding transaction |
order_id | foreignId | The associated order |
success | boolean | Whether the transaction succeeded |
type | enum | One of intent, capture, or refund |
driver | string | The payment driver, e.g. stripe |
amount | integer | Amount in the currency’s smallest unit |
reference | string | Provider reference, e.g. Stripe charge ID |
status | string | Provider-specific status |
notes | string nullable | Any additional notes for the transaction |
card_type | string nullable | Card brand, e.g. visa or mastercard |
last_four | string nullable | Last four digits of the card |
meta | json nullable | Additional provider data |
captured_at | dateTime nullable | When the payment was captured |
created_at | timestamp nullable | |
updated_at | timestamp nullable |
Displaying Transaction History
Offline Payments
For payment methods that do not require online processing (cash on delivery, bank transfer, purchase orders), use the built-inoffline driver:
placed_at, and updates the status to the configured authorized status. No external API calls are made.
Routes
Putting It All Together
Here is a complete checkout controller covering the Stripe payment flow:Next Steps
- Review the Stripe add-on documentation for advanced configuration options.
- Review the Payments reference for the full payment API and transaction model.
- Review the Extending Payments guide for building a custom payment driver.
- Review the Checkout guide for the complete checkout flow including address collection and shipping.