Skip to main content
This guide covers how payment fits into a Lunar storefront: selecting a driver, running the generic authorization flow every driver shares, and reading back the resulting order and transaction state.

Overview

Lunar’s payment system has three layers:
  1. Payment Manager (Lunar\Core\Facades\Payments) — routes payment calls to the correct driver based on configuration
  2. Payment Type (Lunar\Core\Contracts\PaymentType) — handles authorization, capture, and refund logic for a specific provider
  3. Transactions (Lunar\Core\Models\Transaction) — records of every payment event stored against the order
Every driver, first-party or third-party, implements the same PaymentType contract, so storefront code calls the same methods regardless of which provider is behind Payments::driver().

Configuring Payment Types

Payment types are configured in config/lunar/payments.php:
Each entry under types maps a name (used with Payments::driver()) to a driver. Any other keys added to a type’s array are passed to the driver instance via setConfig(), so a driver can read type-specific settings if it needs them; the built-in offline and Stripe/PayPal drivers do not require any.

Registering a Driver

A driver is made available to the payment manager by calling Payments::extend(), typically from a service provider’s boot() method:
The first-party Stripe and PayPal packages call Payments::extend() from their own service providers, which are auto-discovered by Composer — installing lunarphp/stripe or lunarphp/paypal is enough to register the driver. This step only needs to be done manually when building a custom or third-party driver.

The Generic Payment Flow

Every driver is resolved through Payments::driver() and implements Lunar\Core\Contracts\PaymentType:
A checkout only needs to call cart(), withData(), and authorize() to take a payment:
withData() accepts whatever the driver needs to authorize the payment — a Stripe PaymentIntent ID, a PayPal order ID, or nothing at all for an offline driver. authorize() returns a Lunar\Core\DataObjects\PaymentAuthorize object:
An order is only considered “placed” when its placed_at column has a datetime value. Each driver is responsible for setting this once payment succeeds. A draft order (where placed_at is null) has not been placed, even if an order record already exists.
If a draft order does not already exist for the cart, most drivers create one as part of authorize() — see the Checkout guide for the alternative of calling CartSession::createOrder() explicitly before authorizing.

Working Against an Existing Order

Once an order exists (for example, after a failed first attempt), call order() instead of cart():

Amount and Currency Verification

A first-party driver verifies the payment amount and currency against the cart or order total before placing the order, so a client-manipulated amount cannot silently under-charge a customer. Both first-party drivers expose an allow_partial_payment config option for stores that intentionally take deposits or part payments.

Offline Payments

For manual or offline payments (cash on delivery, bank transfer, purchase orders), use the built-in offline driver:
The offline driver creates the order (if one does not already exist) and sets placed_at immediately. No external API calls are made, and no withData() payload is required.

Stripe and PayPal

Lunar ships two first-party payment drivers, lunarphp/stripe and lunarphp/paypal. Both implement the full PaymentType contract, including getPaymentChecks(), verify the authorized amount and currency against the order total, and ship a publishable config file under their own lunar.stripe / lunar.paypal namespace.
  • Stripe — Payment Intents API, supports automatic and manual capture, webhooks, address sync, and payment checks (AVS, postal code, CVC)
  • PayPal — Orders v2 / Payments v2 REST API, server-side only (the storefront renders PayPal’s own JS buttons)
Both guides cover installation, credentials, the provider-specific authorization flow, and webhooks in full. The rest of this guide covers what is common to every driver: the PaymentType contract shown above, and how the order settles afterward. Every other payment gateway is third-party: a package built against the PaymentType contract and registered with Payments::extend(), maintained outside the Lunar monorepo.

Post-Payment Order State

placed_at

An order becomes “placed” the moment its driver sets placed_at. Check this with isPlaced() / isDraft():

payment_status

Unlike placed_at, payment_status is not set directly by a driver. It is a derived rollup, recomputed automatically from the order’s transaction ledger whenever a transaction is created or updated, and cast to a Lunar\Core\States\Order\Payment\PaymentStatus state:

Transactions

Every payment event is recorded as a Lunar\Core\Models\Transaction on the order. Transactions provide a complete audit trail. Transaction uses the same format() / decimal() methods as Order, since amount is stored as a plain integer:

Capture and Refund

For manual-capture flows, or refunding an existing order, prefer the verb methods on Lunar\Core\Models\Order over calling the payment driver directly — they validate the request (matching transaction type, remaining refundable amount, line quantities) before dispatching to the underlying driver.

Manual Capture

capture() takes the transaction ID and a major-unit amount (a decimal, not minor units) — it converts the amount using the order’s currency before handing it to the driver.

Refunds

RefundRequest also accepts shipping and adjustment (both major-unit amounts) for refunding shipping or an amount not tied to specific lines, and notify to control whether the customer receives a refund notification. Both verb methods ultimately call the payment driver’s own capture() / refund() methods from the PaymentType contract, so a custom driver only needs to implement those two methods to support this flow.

Payment Checks

Drivers can attach provider-specific checks (AVS, CVC, and similar) to a transaction. Read them through the transaction rather than the driver directly:
A driver that has nothing to report returns an empty Lunar\Core\DataObjects\PaymentChecks collection — getPaymentChecks() is part of the PaymentType contract, and a driver author should implement it (or explicitly document it as a no-op) rather than omit it.

Routes

Putting It All Together

Here is a checkout controller that authorizes a payment against whichever driver the customer selected, then shows the confirmation page:

Next Steps