Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Payment Adapters

A deeper look into the payment adapter pattern used by the Ecommerce Plugin, and how to create your own.

The current list of supported payment adapters are:

REST API

The plugin will create REST API endpoints for each payment adapter you add to your configuration. The endpoints will be available at /api/payments/{provider_name}/{action} where provider_name is the name of the payment adapter and action is one of the following:

Action

Method

Description

initiate

POST

Initiate a payment for an order. See initiatePayment for more details.

confirm-order

POST

Confirm an order after a payment has been made. See confirmOrder for more details.

Stripe

Out of the box we integrate with Stripe to handle one-off purchases. To use Stripe, you will need to install the Stripe package:

1
pnpm add stripe

We recommend at least 18.5.0 to ensure compatibility with the plugin.

Then, in your plugins array of your Payload Config, call the plugin with:

1
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
2
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
3
import { buildConfig } from 'payload'
4
5
export default buildConfig({
6
// Payload config...
7
plugins: [
8
ecommercePlugin({
9
// rest of config...
10
payments: {
11
paymentMethods: [
12
stripeAdapter({
13
secretKey: process.env.STRIPE_SECRET_KEY,
14
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
15
// Optional - only required if you want to use webhooks
16
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
17
}),
18
],
19
},
20
}),
21
],
22
})

Configuration

The Stripe payment adapter takes the following configuration options:

Option

Type

Description

secretKey

string

Your Stripe Secret Key, found in the Stripe Dashboard.

publishableKey

string

Your Stripe Publishable Key, found in the Stripe Dashboard.

webhookSecret

string

(Optional) Your Stripe Webhooks Signing Secret, found in the Stripe Dashboard. Required if you want to use webhooks.

appInfo

object

(Optional) An object containing name and version properties to identify your application to Stripe.

webhooks

object

(Optional) An object where the keys are Stripe event types and the values are functions that will be called when that event is received. See Webhooks for more details.

groupOverrides

object

(Optional) An object to override the default fields of the payment group. See Payment Fields for more details.

Stripe Webhooks

You can also add your own webhooks to handle events from Stripe. This is optional and the plugin internally does not use webhooks for any core functionality. It receives the following arguments:

Argument

Type

Description

event

Stripe.Event

The Stripe event object

req

PayloadRequest

The Payload request object

stripe

Stripe

The initialized Stripe instance

You can add a webhook like so:

1
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
2
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
3
import { buildConfig } from 'payload'
4
5
export default buildConfig({
6
// Payload config...
7
plugins: [
8
ecommercePlugin({
9
// rest of config...
10
payments: {
11
paymentMethods: [
12
stripeAdapter({
13
secretKey: process.env.STRIPE_SECRET_KEY,
14
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
15
// Required
16
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
17
webhooks: {
18
'payment_intent.succeeded': ({ event, req }) => {
19
console.log({ event, data: event.data.object })
20
req.payload.logger.info('Payment succeeded')
21
},
22
},
23
}),
24
],
25
},
26
}),
27
],
28
})

To use webhooks you also need to have them configured in your Stripe Dashboard.

You can use the Stripe CLI to forward webhooks to your local development environment.

Frontend usage

The most straightforward way to use Stripe on the frontend is with the EcommerceProvider component and the stripeAdapterClient function. Wrap your application in the provider and pass in the Stripe adapter with your publishable key:

1
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react'
2
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
3
4
<EcommerceProvider
5
paymentMethods={[
6
stripeAdapterClient({
7
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
8
}),
9
]}
10
>
11
{children}
12
</EcommerceProvider>

Then you can use the usePayments hook to access the initiatePayment and confirmOrder functions, see the Frontend docs for more details.

Making your own Payment Adapter

You can make your own payment adapter by implementing the PaymentAdapter interface. This interface requires you to implement the following methods:

Property

Type

Description

name

string

The name of the payment method. This will be used to identify the payment method in the API and on the frontend.

label

string

(Optional) A human-readable label for the payment method. This will be used in the admin panel and on the frontend.

initiatePayment

(args: InitiatePaymentArgs) => Promise<InitiatePaymentResult>

The function that is called via the /api/payments/{provider_name}/initiate endpoint to initiate a payment for an order. More

confirmOrder

(args: ConfirmOrderArgs) => Promise<void>

The function that is called via the /api/payments/{provider_name}/confirm-order endpoint to confirm an order after a payment has been made. More

endpoints

Endpoint[]

(Optional) An array of endpoints to be bootstrapped to Payload's API in order to support the payment method. All API paths are relative to /api/payments/{provider_name}

group

GroupField

A group field config to be used in transactions to track the necessary data for the payment processor, eg. PaymentIntentID for Stripe. See Payment Fields for more details.

The arguments can be extended but should always include the PaymentAdapterArgs type which has the following types:

Property

Type

Description

label

string

(Optional) Allow overriding the default UI label for this adaper.

groupOverrides

FieldsOverride

(Optional) Allow overriding the default fields of the payment group. See Payment Fields for more details.

initiatePayment

The initiatePayment function is called when a payment is initiated. At this step the transaction is created with a status "Processing", an abandoned purchaase will leave this transaction in this state. It receives an object with the following properties:

Property

Type

Description

transactionsSlug

Transaction

The transaction being processed.

data

object

The cart associated with the transaction.

customersSlug

string

The customer associated with the transaction.

req

PayloadRequest

The Payload request object.

The data object will contain the following properties:

Property

Type

Description

billingAddress

Address

The billing address associated with the transaction.

shippingAddress

Address

(Optional) The shipping address associated with the transaction. If this is missing then use the billing address.

cart

Cart

The cart collection item.

customerEmail

string

In the case that req.user is missing, customerEmail should be required in order to process guest checkouts.

currency

string

The currency for the cart associated with the transaction.

The return type then only needs to contain the following properties though the type supports any additional data returned as needed for the frontend:

Property

Type

Description

message

string

A success message to be returned to the client.

At any point in the function you can throw an error to return a 4xx or 5xx response to the client.

A heavily simplified example of implementing initiatePayment could look like:

1
import {
2
PaymentAdapter,
3
PaymentAdapterArgs,
4
} from '@payloadcms/plugin-ecommerce'
5
import Stripe from 'stripe'
6
7
export const initiatePayment: NonNullable<PaymentAdapter>['initiatePayment'] =
8
async ({ data, req, transactionsSlug }) => {
9
const payload = req.payload
10
11
// Check for any required data
12
const currency = data.currency
13
const cart = data.cart
14
15
if (!currency) {
16
throw new Error('Currency is required.')
17
}
18
19
const stripe = new Stripe(secretKey)
20
21
try {
22
let customer = (
23
await stripe.customers.list({
24
email: customerEmail,
25
})
26
).data[0]
27
28
// Ensure stripe has a customer for this email
29
if (!customer?.id) {
30
customer = await stripe.customers.create({
31
email: customerEmail,
32
})
33
}
34
35
const shippingAddressAsString = JSON.stringify(shippingAddressFromData)
36
37
const paymentIntent = await stripe.paymentIntents.create()
38
39
// Create a transaction for the payment intent in the database
40
const transaction = await payload.create({
41
collection: transactionsSlug,
42
data: {},
43
})
44
45
// Return the client_secret so that the client can complete the payment
46
const returnData: InitiatePaymentReturnType = {
47
clientSecret: paymentIntent.client_secret || '',
48
message: 'Payment initiated successfully',
49
paymentIntentID: paymentIntent.id,
50
}
51
52
return returnData
53
} catch (error) {
54
payload.logger.error(error, 'Error initiating payment with Stripe')
55
56
throw new Error(
57
error instanceof Error
58
? error.message
59
: 'Unknown error initiating payment',
60
)
61
}
62
}

confirmOrder

The confirmOrder function is called after a payment is completed on the frontend and at this step the order is created in Payload. It receives the following properties:

Property

Type

Description

ordersSlug

string

The orders collection slug.

transactionsSlug

string

The transactions collection slug.

cartsSlug

string

The carts collection slug.

customersSlug

string

The customers collection slug.

data

object

The cart associated with the transaction.

req

PayloadRequest

The Payload request object.

The data object will contain any data the frontend chooses to send through and at a minimum the following:

Property

Type

Description

customerEmail

string

In the case that req.user is missing, customerEmail should be required in order to process guest checkouts.

The return type can also contain any additional data with a minimum of the following:

Property

Type

Description

message

string

A success message to be returned to the client.

orderID

string

The ID of the created order.

transactionID

string

The ID of the associated transaction.

A heavily simplified example of implementing confirmOrder could look like:

1
import {
2
PaymentAdapter,
3
PaymentAdapterArgs,
4
} from '@payloadcms/plugin-ecommerce'
5
import Stripe from 'stripe'
6
7
export const confirmOrder: NonNullable<PaymentAdapter>['confirmOrder'] =
8
async ({
9
data,
10
ordersSlug = 'orders',
11
req,
12
transactionsSlug = 'transactions',
13
}) => {
14
const payload = req.payload
15
16
const customerEmail = data.customerEmail
17
const paymentIntentID = data.paymentIntentID as string
18
19
const stripe = new Stripe(secretKey)
20
21
try {
22
// Find our existing transaction by the payment intent ID
23
const transactionsResults = await payload.find({
24
collection: transactionsSlug,
25
where: {
26
'stripe.paymentIntentID': {
27
equals: paymentIntentID,
28
},
29
},
30
})
31
32
const transaction = transactionsResults.docs[0]
33
34
// Verify the payment intent exists and retrieve it
35
const paymentIntent =
36
await stripe.paymentIntents.retrieve(paymentIntentID)
37
38
// Create the order in the database
39
const order = await payload.create({
40
collection: ordersSlug,
41
data: {},
42
})
43
44
const timestamp = new Date().toISOString()
45
46
// Update the cart to mark it as purchased, this will prevent further updates to the cart
47
await payload.update({
48
id: cartID,
49
collection: 'carts',
50
data: {
51
purchasedAt: timestamp,
52
},
53
})
54
55
// Update the transaction with the order ID and mark as succeeded
56
await payload.update({
57
id: transaction.id,
58
collection: transactionsSlug,
59
data: {
60
order: order.id,
61
status: 'succeeded',
62
},
63
})
64
65
return {
66
message: 'Payment initiated successfully',
67
orderID: order.id,
68
transactionID: transaction.id,
69
}
70
} catch (error) {
71
payload.logger.error(error, 'Error initiating payment with Stripe')
72
}
73
}

Payment Fields

Payment fields are used primarily on the transactions collection to store information about the payment method used. Each payment adapter must provide a group field which will be used to store this information.

For example, the Stripe adapter provides the following group field:

1
const groupField: GroupField = {
2
name: 'stripe',
3
type: 'group',
4
admin: {
5
condition: (data) => {
6
const path = 'paymentMethod'
7
8
return data?.[path] === 'stripe'
9
},
10
},
11
fields: [
12
{
13
name: 'customerID',
14
type: 'text',
15
label: 'Stripe Customer ID',
16
},
17
{
18
name: 'paymentIntentID',
19
type: 'text',
20
label: 'Stripe PaymentIntent ID',
21
},
22
],
23
}

Client side Payment Adapter

The client side adapter should implement the PaymentAdapterClient interface:

Property

Type

Description

name

string

The name of the payment method. This will be used to identify the payment method in the API and on the frontend.

label

string

(Optional) A human-readable label for the payment method. This can be used as a human readable format.

initiatePayment

boolean

Flag to toggle on the EcommerceProvider's ability to call the /api/payments/{provider_name}/initiate endpoint. If your payment method does not require this step, set this to false.

confirmOrder

boolean

Flag to toggle on the EcommerceProvider's ability to call the /api/payments/{provider_name}/confirm-order endpoint. If your payment method does not require this step, set this to false.

And for the args use the PaymentAdapterClientArgs type:

Property

Type

Description

label

string

(Optional) Allow overriding the default UI label for this adaper.

Best Practices

Always handle sensitive operations like creating payment intents and confirming payments on the server side. Use webhooks to listen for events from Stripe and update your orders accordingly. Never expose your secret key on the frontend. By default Nextjs will only expose environment variables prefixed with NEXT_PUBLIC_ to the client.

While we validate the products and prices on the server side when creating a payment intent, you should override the validation function to add any additional checks you may need for your specific use case.

You are safe to pass the ID of a transaction to the frontend however you shouldn't pass any sensitive information or the transaction object itself.

When passing price information to your payment provider it should always come from the server and it should be verified against the products in your database. Never trust price information coming from the client.

When using webhooks, ensure that you verify the webhook signatures to confirm that the requests are genuinely from Stripe. This helps prevent unauthorized access and potential security vulnerabilities.

Next

Advanced uses and examples