Stripe Payment Gateway

Accept credit and debit card payments through Stripe. This add-on integrates Stripe’s Payment Intents API with the Larapen e-shop, providing secure, PCI-compliant card processing with 3D Secure support, webhook-driven order updates, and one-click refunds.

Payment Intents API

Uses Stripe’s latest Payment Intents flow for SCA-compliant, secure card payments.

3D Secure

Automatic or always-on 3D Secure verification for strong customer authentication (SCA).

Webhook Processing

Handles payment success, failure, refund, and dispute events via signed webhooks.

Encrypted Credentials

API keys are encrypted at rest using Laravel’s Crypt facade. Never stored in plaintext.

Use Cases

Online Store with Card Payments

You run an e-shop on Larapen selling physical or digital products. Customers select “Credit Card” at checkout, enter their card details in a Stripe Elements form, and pay instantly.

  • Install the Stripe add-on alongside the Shop add-on.
  • Enter your Stripe API keys in Admin → Stripe → Settings.
  • Stripe appears as a payment option at checkout automatically.
  • Orders are confirmed in real time via webhooks.

Digital Downloads with Instant Delivery

You sell digital products (e-books, software, templates). Stripe confirms payment immediately, triggering order completion and granting download access.

Multi-Currency International Sales

Configure the currency to match your target market (USD, EUR, GBP, etc.). Stripe handles currency conversion and international card networks.

Requirements

  • Larapen CMS v1.0.0 or later
  • PHP 8.3+
  • MySQL 8.0+
  • The Shop add-on (required dependency)
  • A Stripe account with API keys (see Obtaining API Keys)
  • The stripe/stripe-php Composer package
Note: The Stripe add-on depends on the Shop add-on. It registers itself as a payment gateway via the PaymentGatewayInterface contract and is automatically discovered by the shop checkout system.

Installation

Step 1: Place the Add-on

Copy or symlink the stripe folder into your Larapen "extensions/addons" directory:

Step 2: Install Stripe PHP SDK

Step 3: Activate the Add-on

Go to Admin → Add-ons → Installed Add-ons and activate Stripe Payment Gateway.

Step 4: Run Migrations

This creates 2 tables: stripe_customers and stripe_payment_intents.

Step 5: Configure API Keys

Navigate to Admin → Stripe → Settings and enter your Stripe Publishable Key, Secret Key, and (optionally) the Webhook Signing Secret. See Configuration.

Step 6: Set Up Webhooks

In the Stripe Dashboard, create a webhook endpoint pointing to:

Select the events listed in Handled Events. Copy the signing secret and paste it in the admin settings. See Webhook Setup.

Configuration

All settings are managed in Admin → Stripe → Settings (stored in the settings table, group stripe).

Setting Description Default
stripe_public_key Stripe Publishable Key (starts with pk_). Encrypted at rest. (empty)
stripe_secret_key Stripe Secret Key (starts with sk_). Encrypted at rest. (empty)
stripe_webhook_secret Webhook signing secret (starts with whsec_). Encrypted at rest. (empty)
stripe_capture_method How payments are captured: automatic (immediate) or manual (authorize then capture later). automatic
stripe_currency Three-letter ISO currency code in lowercase (e.g. usd, eur, gbp). usd
stripe_request_three_d_secure When to request 3D Secure: automatic (only when required by the issuing bank) or any (always request). automatic
Security: API keys are encrypted using Laravel’s Crypt::encryptString() before being stored in the database. They are decrypted at runtime when needed. Never share your secret key.

Environment Variables

Environment variables serve as defaults. Settings saved in the admin panel override them.

Note: Environment variables are used as fallback defaults when no value is saved in the admin panel. Admin panel settings always take priority and are stored encrypted.

Obtaining API Keys

  1. Log in to the Stripe Dashboard.
  2. Navigate to Developers → API Keys.
  3. Copy the Publishable key (starts with pk_test_ or pk_live_).
  4. Reveal and copy the Secret key (starts with sk_test_ or sk_live_).
  5. Paste both keys in Admin → Stripe → Settings.
Test vs. Live: Use test keys (pk_test_ / sk_test_) during development. Switch to live keys for production. Test card numbers are available at docs.stripe.com/testing.

Test Card Numbers

Card Number Scenario
4242 4242 4242 4242 Successful payment
4000 0025 0000 3155 Requires 3D Secure authentication
4000 0000 0000 9995 Declined (insufficient funds)

Use any future expiry date (e.g. 12/34) and any 3-digit CVC.

Admin: Settings

The settings page (Admin → Stripe → Settings) is organized into two sections:

API Keys

  • Publishable Key: Masked password field with show/hide toggle. Starts with pk_.
  • Secret Key: Masked password field with show/hide toggle. Starts with sk_. Encrypted before storage.
  • Webhook Signing Secret: Masked password field. Starts with whsec_. Used to verify incoming webhook signatures.

A warning banner reminds admins that keys are encrypted before storage and that fields should be left empty to retain current values.

An info box provides direct links to:

Payment Options

  • Capture Method: Automatic (charge immediately) or Manual (authorize now, capture later).
  • Currency: Three-letter currency code in lowercase (e.g. usd, eur, gbp).
  • 3D Secure: Automatic (only when required by the issuing bank) or Always request.

Payment Flow

The Stripe add-on uses the Payment Intents API for SCA-compliant card processing. Here is the complete payment flow:

  1. Checkout Selection: The customer selects “Credit Card” as the payment method at checkout. The Stripe payment form (Stripe Elements) appears.
  2. Order Creation: The shop creates an order and calls StripeGateway::createPaymentIntent($order).
  3. Payment Intent: The gateway creates a Stripe PaymentIntent via the API, stores the details locally in stripe_payment_intents, and returns the client_secret.
  4. Client-Side Confirmation: The browser uses stripe.confirmCardPayment() with the client secret and card details collected via Stripe Elements.
  5. 3D Secure: If the card requires authentication, Stripe displays a 3D Secure challenge. The customer completes it in a modal.
  6. Server Confirmation: On success, the browser redirects to /stripe/confirm?payment_intent={id}. The StripeController::confirm() method retrieves the PaymentIntent from Stripe, updates the local record, and marks the order as paid.
  7. Webhook Backup: Stripe also sends a payment_intent.succeeded webhook. This ensures the order is marked paid even if the customer closes the browser before the redirect completes.
Dual Confirmation: The system uses both client-side redirect confirmation and server-side webhooks. The webhook acts as a safety net: whichever arrives first marks the order as paid; the second is a no-op.

Payment Confirmation

After client-side card confirmation succeeds, the browser redirects to the server-side confirmation endpoint:

GET /stripe/confirm?payment_intent={id}
Description

Retrieves the PaymentIntent from Stripe, verifies its status, updates the local record, and marks the associated order as paid.

Possible Outcomes
succeeded Order marked as paid. Redirect to success page.
requires_action Additional authentication needed. Redirect back to checkout with client secret.
Other Payment failed. Redirect to checkout with error message.

Webhook Setup

Webhooks ensure your site receives payment status updates even if the customer closes their browser.

Creating a Webhook Endpoint in Stripe

  1. Go to Stripe Dashboard → Developers → Webhooks.
  2. Click Add endpoint.
  3. Enter your endpoint URL: https://yoursite.com/stripe/webhook
  4. Select the following events:
    • payment_intent.succeeded
    • payment_intent.payment_failed
    • charge.refunded
    • charge.dispute.created
  5. Click Add endpoint to save.
  6. Reveal the Signing secret (starts with whsec_) and paste it in Admin → Stripe → Settings.
Important: The webhook endpoint (/stripe/webhook) is excluded from CSRF verification. Authentication is handled via Stripe’s signature verification instead.

Processing Refunds

Refunds are processed through the StripeGateway::refund() method, which can be called from the shop’s order management interface.

Refund Flow

  1. Admin initiates a refund from the order detail page in the admin panel.
  2. The system calls StripeGateway::refund($transaction, $amount, $reason).
  3. A Stripe\Refund is created via the API using the original PaymentIntent ID.
  4. A new Transaction record is created with type refund.
  5. The refund result is returned (success, pending, or failure).

Partial Refunds

The $amount parameter allows partial refunds. The amount is specified in the order’s currency (not in cents: the gateway handles cent conversion internally).

Refund Statuses

Status Description
succeeded Refund processed immediately. Transaction status: completed.
pending Refund is being processed (can take 5–10 business days for some payment methods). Transaction status: pending.
failed Refund could not be processed. Error details logged.

Updating

Step 1: Replace Files

Replace the add-on directory with the new version.

Step 2: Update Stripe PHP SDK

Step 3: Run Migrations

Step 4: Clear Caches

Step 5: Verify

Visit Admin → Stripe → Settings and confirm your API keys are still configured. Process a test payment to verify the integration.

Backup first: Always back up your database before running migrations on a production system.

Troubleshooting

Stripe not appearing as a payment method at checkout

  • Ensure the Stripe add-on is activated in Admin → Add-ons.
  • Ensure the Shop add-on is also activated (Stripe depends on it).
  • Verify that both stripe_public_key and stripe_secret_key are configured. The isAvailable() method returns false if either is empty.

Payment fails with “Invalid API Key”

  • Check that the secret key starts with sk_test_ (test mode) or sk_live_ (production).
  • If you recently rotated your keys in the Stripe Dashboard, update them in the admin settings.
  • Ensure the key is properly encrypted. Try clearing the value and re-entering it.

Webhooks returning 400 errors

  • Verify the stripe_webhook_secret is set in admin settings.
  • The secret must match the specific endpoint you created in the Stripe Dashboard (each endpoint has its own unique signing secret).
  • Ensure your server’s clock is synchronized (Stripe rejects signatures with excessive time drift).
  • Check that the webhook URL is https://yoursite.com/stripe/webhook (not http://).

3D Secure challenge not appearing

  • In test mode, use the test card 4000 0025 0000 3155 which always requires 3D Secure.
  • With automatic mode, 3DS only triggers when the issuing bank requires it. Switch to any to force it for testing.
  • Ensure Stripe.js is loaded correctly. Check the browser console for JavaScript errors.

Order not marked as paid after successful payment

  • Check server logs for errors during the /stripe/confirm redirect.
  • Verify the webhook is configured and receiving events. Check the Stripe webhook logs for delivery attempts.
  • Ensure the stripe_payment_intents table has a record for the PaymentIntent ID. If not, the payment was not initiated through the standard flow.
  • Check that the Payable interface is correctly implemented on the Order model with a working markAsPaid() method.

Refund fails with “No such payment intent”

  • The refund uses the gateway_transaction_id from the Transaction record. Ensure this contains the original Stripe PaymentIntent ID (starts with pi_).
  • You cannot refund a PaymentIntent that has not yet succeeded.

Customer created on every payment

  • The getOrCreateCustomer() method checks for an existing stripe_customers record by user_id before creating a new Stripe Customer. If duplicates appear, verify the user_id foreign key is set correctly.
  • Guest checkouts (no authenticated user) do not create Stripe Customer objects.

Currency mismatch errors

  • The stripe_currency setting must match the currency used by the shop. For example, if the shop prices are in EUR, set the Stripe currency to eur.
  • Currency codes must be lowercase (e.g. usd, not USD).

Was this article helpful?

Thank you for your feedback!

Still need help? Create a support ticket

Create a Ticket