Flexible payment processing for your Craft Commerce store, powered by Stripe.
This plugin provides a gateway that leverages the Payment Intents API to support popular payment methods like…
- Major debit and credit cards
- Apple Pay
- Google Pay
- Cash App
- Afterpay, Affirm, and other installment plans
- ECH and direct bank account transfers
…and more!
Note
Looking for 3.x documentation?
- Craft CMS 4.0 or later
- Craft Commerce 4.3 or later
- Stripe API version
2022-11-15
You can install this plugin from the Plugin Store or with Composer.
Go to the Plugin Store in your project’s control panel, search for “Stripe for Craft Commerce”, and click Install in the sidebar.
Open your terminal and run the following commands:
# Switch your project’s directory:
cd /path/to/my-project
# Require the package with Composer:
composer require craftcms/commerce-stripe
# Install the plugin with Craft:
php craft install/plugin commerce-stripe
To add a Stripe payment gateway, open the Craft control panel, navigate to Commerce → System Settings → Gateways, and click + New gateway.
Your gateway’s Name should make sense to administrators and customers (especially if you’re using the example templates).
From the Gateway dropdown, select Stripe, then provide the following information:
- Publishable API Key
- Secret API Key
- Webhook Signing Secret (See Webhooks for details)
Your Publishable API Key and Secret API Key can be found in (or generated from) your Stripe dashboard, within the Developers → API Keys tab. Read more about Stripe API keys.
Note
To prevent secrets leaking into project config, put them in your .env
file, then use the special environment variable syntax in the gateway settings.
Stripe provides different keys for testing—use those until you are ready to launch, then replace the testing keys in the live server’s .env
file.
Once the gateway has been saved (and it has an ID), revisiting its edit screen will reveal a Webhook URL that can be copied into a new webhook in your Stripe dashboard. A signing secret will be generated for you—save this in your .env
file with the other secrets, then return to the gateway’s settings screen and populate the Webhook Signing Secret field with the variable’s name.
Warning
Webhooks will not be processed if the signing secret is missing or invalid!
We recommend enabling all available events for the webhook, in Stripe. Events that the plugin has no use for will be ignored.
Remember that the webhook URL will be different for each of your environments! The gateway itself may have a different ID in production than in development, due to the way Project Config works).
Your local environment isn’t often exposed to the public internet, so Stripe won’t be able to send webhooks for testing. You have two options for testing webhooks:
-
Use the Stripe CLI, replacing the URL in the command below with the one from your gateway’s setting screen:
stripe listen --forward-to "my-project.ddev.site/index.php?action=commerce/webhooks/process-webhook&gateway=1"
This command will create a temporary webhook and signing secret, which you should add to your
.env
file and See Stripe’s Testing Webhooks article for more information. -
Use DDEV’s
share
command, and use the public Ngrok URL when configuring the webhook manually, in Stripe.
Version 4.0 is largely backward-compatible with 3.x. Review the following sections to ensure your site (and any customizations) remain functional.
To support the full array of payment methods available via Stripe (like Apple Pay and Google Pay), the plugin makes exclusive use of the Payment Intents API.
Historically, gateway.getPaymentFormHtml()
has output a basic form for tokenizing a credit card, client-side—it would then submit only the resulting token to the commerce/payments/pay
action, and capture the payment from the back-end. Custom payment forms that use this process will continue to work.
Now, output is a more flexible Payment Element form, which makes use of Stripes modern Payment Intents API. The process looks something like this:
- A request is submitted in the background to
commerce/payments/pay
(without a payment method); - Commerce creates a Payment Intent with some information about the order, then sets up an internal
Transaction
record to track its status; - The Payment Intent’s
client_secret
is returned to the front-end; - The Stripe JS SDK is initialized with the secret, and the customer is able to select from the available payment methods;
Details on how to configure the new payment form are below.
Your Stripe account must be configured to use at least version 2022-11-15
of their API, due to the availability of certain Payment Intents features.
Support for creation of new payment sources in the same request as a subscription has been deprecated due to inconsistencies with Stripe’s handling of default payment methods. In future versions, the subscription endpoint will focus solely on starting a subscription, not accepting payment information. For now, custom subscription forms that use the legacy Charge workflow will continue to work.
We recommend one of the following strategies:
-
Set up a payment source before choosing a subscription. Design your subscription process to capture payment details, then select from plans.
-
Create a payment source over Ajax on the same page. This should only be supported when configuring the customer’s first payment method. You may preflight an Ajax request to Commerce’s
payment-sources/add
action to set up a payment method, then use the subscription form normally—Stripe will use that sole payment source with the subscription. -
Show users what payment method the subscription will be associated with. This is a great idea, regardless—confirm to the user which of their existing payment sources will be used. You can find the customer’s default payment source in Twig, like this:
{# Assuming a `plan` variable exists in this context... #} {% set paymentSources = craft.commerce.paymentSources.getAllGatewayPaymentSourcesByCustomerId(plan.gatewayId, currentUser.id) %} {% set primaryPaymentSource = paymentSources | filter((ps) => ps.getIsPrimary()) | first %} {% if primaryPaymentSource %} {# Show some information about the source: #} This subscription will be billed to: {{ primaryPaymentSource.description }} {# Then, output the form! #} {% else %} <p>You must set up a payment method to start a subscription!</p> {{ tag('a', { href: siteUrl('account/payment-sources'), text: 'Add a payment method', }) }} {% endif %}
[!NOTE] If your store uses multiple gateways, the customer’s default payment source may not always belong to the same gateway as the plan, so
primaryPaymentSource
can be empty, even if they’ve selected one.
After the update, we recommend running the new payment method synchronization command to ensure your store’s data is up-to-date with Stripe’s records.
These options are set via config/commerce-stripe.php
.
For subscriptions with automatic payments, Stripe creates an invoice 1-2 hours before attempting to charge it. By setting this to true
, you can force Stripe to charge this invoice immediately.
Warning
This setting affects all Stripe gateways in your Commerce installation.
The Stripe plugin provides an interface between Commerce’s subscriptions system and Stripe’s Billing APIs.
Plans must first be configured in the Stripe dashboard.
- In the Craft control panel, navigate to Commerce → Store Settings → Plans, and click + New subscription plan;
- Select Stripe from the Gateway dropdown;
- Choose a plan name from the Gateway plan dropdown;
Note
Plans in Stripe are configured separately for live and test mode! You may see a different list of plans depending on which keys you’re working with.
In addition to the values you POST to Commerce’s commerce/subscriptions/subscribe
action, the Stripe gateway supports these options:
The first full billing cycle will start once the number of trial days lapse. Default value is 0
.
In addition to the values you POST to Commerce’s commerce/subscriptions/cancel
action, the Stripe gateway supports these options:
If this parameter is set to true
, the subscription is canceled immediately. Stripe considers this a simultaneous cancellation and “deletion” (as far as webhooks are concerned)—but a record of the subscription remains available. By default, the subscription is marked as canceled and will end along with the current billing cycle. Defaults to false
.
Note
Immediately canceling a subscription means it cannot be reactivated.
In addition to the values you POST to Commerce’s commerce/subscriptions/switch
action, the Stripe gateway supports these options:
If this parameter is set to true
, the subscription switch will be prorated. Defaults to false
.
If this parameter is set to true
, the subscription switch is billed immediately. Otherwise, the cost (or credit, if prorate
is set to true
when switching to a cheaper plan) is applied to the next invoice.
Warning
If the billing periods differ, the plan switch will be billed immediately and this parameter will be ignored.
There are no customizations available when reactivating a subscription.
The plugin provides several events you can use to modify the behavior of your integration.
Plugins get a chance to provide additional metadata when communicating with Stripe in the course of creating a Payment Intent. This gives you near-complete control over the data that Stripe sees, with the following considerations:
- Changes to the
Transaction
model (available via the event’stransaction
property) will not be saved; - The gateway automatically sets
order_id
,order_number
,order_short_number
,transaction_id
,transaction_reference
,description
, andclient_ip
metadata keys; - Changes to the
amount
andcurrency
keys under therequest
property will be ignored, as these are essential to the gateway functioning in a predictable way;
use craft\commerce\models\Transaction;
use craft\commerce\stripe\events\BuildGatewayRequestEvent;
use craft\commerce\stripe\gateways\PaymentIntents;
use yii\base\Event;
Event::on(
PaymentIntents::class,
PaymentIntents::EVENT_BUILD_GATEWAY_REQUEST,
function(BuildGatewayRequestEvent $e) {
/** @var Transaction $transaction */
$transaction = $e->transaction;
$order = $transaction->getOrder();
$e->request['metadata']['shipping_method'] = $order->shippingMethodHandle;
}
);
Note
Subscription events are handled separately.
In addition to the generic craft\commerce\services\Webhooks::EVENT_BEFORE_PROCESS_WEBHOOK
event, you can listen to craft\commerce\stripe\gateways\PaymentIntents::EVENT_RECEIVE_WEBHOOK
. This event is only emitted after validating a webhook’s authenticity—but it doesn’t make any indication about whether an action was taken in response to it.
use craft\commerce\stripe\events\ReceiveWebhookEvent;
use craft\commerce\stripe\gateways\PaymentIntents;
use yii\base\Event;
Event::on(
PaymentIntents::class,
PaymentIntents::EVENT_RECEIVE_WEBHOOK,
function(ReceiveWebhookEvent $e) {
if ($e->webhookData['type'] == 'charge.dispute.created') {
if ($e->webhookData['data']['object']['amount'] > 1000000) {
// Be concerned that a USD 10,000 charge is being disputed.
}
}
}
);
webhookData
will always have a type
key, which determines the schema of everything within data
. Check the Stripe documentation for what kinds of data to expect.
Plugins get a chance to do something when a Stripe invoice is created. This is typically emitted in the course of handling a webhook.
use craft\commerce\stripe\events\CreateInvoiceEvent;
use craft\commerce\stripe\gateways\PaymentIntents;
use yii\base\Event;
Event::on(
PaymentIntents::class,
PaymentIntents::EVENT_CREATE_INVOICE,
function(CreateInvoiceEvent $e) {
if ($e->invoiceData['billing'] === 'send_invoice') {
// Forward this invoice to the accounting department.
}
}
);
Plugins get a chance to tweak subscription parameters when subscribing.
use craft\commerce\stripe\events\SubscriptionRequestEvent;
use craft\commerce\stripe\gateways\PaymentIntents;
use yii\base\Event;
Event::on(
PaymentIntents::class,
PaymentIntents::EVENT_BEFORE_SUBSCRIBE,
function(SubscriptionRequestEvent $e) {
/** @var craft\commerce\base\Plan $plan */
$plan = $e->plan;
/** @var craft\elements\User $user */
$user = $e->user;
// Add something to the metadata:
$e->parameters['metadata']['name'] = $user->fullName;
unset($e->parameters['metadata']['another_property']);
}
);
Commerce also has a generic subscription event that is emitted for subscriptions created via any gateway.
You can now generate a link to the Stripe billing portal for customers to manage their credit cards and plans.
<a href={{ gateway.billingPortalUrl(currentUser) }}">Manage your billing account</a>
Pass a returnUrl
parameter to return the customer to a specific on-site page after they have finished:
{{ gateway.billingPortalUrl(currentUser, 'myaccount') }}
A specific Stripe customer portal configuration can be chosen with the third configurationId
argument. This value must agree with a preexisting configuration created via the API in the associated Stripe account:
{{ gateway.billingPortalUrl(currentUser, 'myaccount', 'config_12345') }}
Note
Logged-in users can also be redirected with the commerce-stripe/customers/billing-portal-redirect
action. The configurationId
parameter is not supported when using this method.
Payment methods created directly in the Stripe customer portal are now synchronized back to Commerce. Customers’ primary payment methods are also synchronized.
Note
Webhooks must be configured for this to work as expected!
To perform an initial sync, run the commerce-stripe/sync/payment-sources
console command:
php craft commerce-stripe/sync/payment-sources
To render a Stripe Elements payment form, get a reference to the gateway, then call its getPaymentFormHtml()
method:
{% set cart = craft.commerce.carts.cart %}
{% set gateway = cart.gateway %}
<form method="POST">
{{ csrfInput() }}
{{ actionInput('commerce/payments/pay') }}
{% namespace gateway.handle|commercePaymentFormNamespace %}
{{ gateway.getPaymentFormHtml({})|raw }}
{% endnamespace %}
<button>Pay</button>
</form>
This assumes you have provided a means of selecting the gateway in a prior checkout step. If your store only uses a single gateway, you may get a reference to the gateway statically and set it during payment:
{% set gateway = craft.commerce.gateways.getGatewayByHandle('myStripeGateway') %}
<form method="POST">
{{ csrfInput() }}
{{ actionInput('commerce/payments/pay') }}
{# Include *outside* the namespaced form inputs: #}
{{ hiddenInput('gatewayId', gateway.id) }}
{% namespace gateway.handle|commercePaymentFormNamespace %}
{{ gateway.getPaymentFormHtml({})|raw }}
{% endnamespace %}
<button>Pay</button>
</form>
Regardless of how you use this output, it will automatically register all the necessary Javascript to create the Payment Intent and bootstrap Stripe Elements.
getPaymentFormHtml()
accepts an array with any of the following keys:
Renders a Stripe Elements form with all the payment method types enabled in your Stripe Dashboard. Some methods may be hidden if the order total or currency don’t meet the method’s criteria—or if they aren’t supported in the current environment.
{% set params = {
paymentFormType: 'elements',
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
This is the default paymentFormType
, and most installations will not need to declare or change it.
This generates a form ready to redirect to a hosted Stripe Checkout page. This can only be used inside a commerce/payments/pay
form. This option ignores all other params.
{% set params = {
paymentFormType: 'checkout',
} %}
{{ gateway.getPaymentFormHtml(params)|raw }}
You can pass an array of appearance options to the stripe.elements()
configurator function. This expects data compatible with the Elements Appearance API.
{% set params = {
appearance: {
theme: 'stripe'
}
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
{% set params = {
appearance: {
theme: 'night',
variables: {
colorPrimary: '#0570de'
}
}
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
Modify the Payment Element options passed to the elements.create()
factory function.
{% set params = {
elementOptions: {
layout: {
type: 'tabs',
defaultCollapsed: false,
radios: false,
spacedAccordionItems: false
}
}
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
The default elementOptions
value only defines a layout:
{% set params = {
elementOptions: {
layout: {
type: 'tabs'
}
}
} %}
Error messages are displayed in a container above the form. You can add classes to this element to alter its style.
{% set params = {
errorMessageClasses: 'bg-red-200 text-red-600 my-2 p-2 rounded',
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}
Customize the style and text of the button used to submit a payment form.
{% set params = {
submitButtonClasses: 'cursor-pointer rounded px-4 py-2 inline-block bg-blue-500 hover:bg-blue-600 text-white hover:text-white my-2',
submitButtonText: 'Pay',
} %}
{{ cart.gateway.getPaymentFormHtml(params)|raw }}