1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423---
title: Payment Adapters
label: Payment Adapters
order: 40
desc: Add ecommerce functionality to your Payload CMS application with this plugin.
keywords: plugins, ecommerce, stripe, plugin, payload, cms, shop, payments
---
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:
- [Stripe](#stripe)
## 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](#initiatePayment) for more details. |
| `confirm-order` | POST | Confirm an order after a payment has been made. See [confirmOrder](#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:
```bash
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](https://payloadcms.com/docs/configuration/overview), call the plugin with:
```ts
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
import { buildConfig } from 'payload'
export default buildConfig({
// Payload config...
plugins: [
ecommercePlugin({
// rest of config...
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
// Optional - only required if you want to use webhooks
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
}),
],
},
}),
],
})
```
### Configuration
The Stripe payment adapter takes the following configuration options:
| Option | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| secretKey | `string` | Your Stripe Secret Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). |
| publishableKey | `string` | Your Stripe Publishable Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). |
| webhookSecret | `string` | (Optional) Your Stripe Webhooks Signing Secret, found in the [Stripe Dashboard](https://dashboard.stripe.com/webhooks). 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](#stripe-webhooks) for more details. |
| groupOverrides | `object` | (Optional) An object to override the default fields of the payment group. See [Payment Fields](./advanced#payment-fields) for more details. |
### Stripe Webhooks
You can also add your own webhooks to handle [events from Stripe](https://docs.stripe.com/api/events). 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:
```ts
import { ecommercePlugin } from '@payloadcms/plugin-ecommerce'
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
import { buildConfig } from 'payload'
export default buildConfig({
// Payload config...
plugins: [
ecommercePlugin({
// rest of config...
payments: {
paymentMethods: [
stripeAdapter({
secretKey: process.env.STRIPE_SECRET_KEY,
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
// Required
webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET,
webhooks: {
'payment_intent.succeeded': ({ event, req }) => {
console.log({ event, data: event.data.object })
req.payload.logger.info('Payment succeeded')
},
},
}),
],
},
}),
],
})
```
To use webhooks you also need to have them configured in your Stripe Dashboard.
You can use the [Stripe CLI](https://stripe.com/docs/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:
```ts
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react'
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
<EcommerceProvider
paymentMethods={[
stripeAdapterClient({
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '',
}),
]}
>
{children}
</EcommerceProvider>
```
Then you can use the `usePayments` hook to access the `initiatePayment` and `confirmOrder` functions, see the [Frontend docs](./frontend#usePayments) 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](#initiatePayment) |
| `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](#confirmOrder) |
| `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](#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 adapter. |
| `groupOverrides` | `FieldsOverride` | (Optional) Allow overriding the default fields of the payment group. See [Payment Fields](#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 purchase 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:
```ts
import {
PaymentAdapter,
PaymentAdapterArgs,
} from '@payloadcms/plugin-ecommerce'
import Stripe from 'stripe'
export const initiatePayment: NonNullable<PaymentAdapter>['initiatePayment'] =
async ({ data, req, transactionsSlug }) => {
const payload = req.payload
// Check for any required data
const currency = data.currency
const cart = data.cart
if (!currency) {
throw new Error('Currency is required.')
}
const stripe = new Stripe(secretKey)
try {
let customer = (
await stripe.customers.list({
email: customerEmail,
})
).data[0]
// Ensure stripe has a customer for this email
if (!customer?.id) {
customer = await stripe.customers.create({
email: customerEmail,
})
}
const shippingAddressAsString = JSON.stringify(shippingAddressFromData)
const paymentIntent = await stripe.paymentIntents.create()
// Create a transaction for the payment intent in the database
const transaction = await payload.create({
collection: transactionsSlug,
data: {},
})
// Return the client_secret so that the client can complete the payment
const returnData: InitiatePaymentReturnType = {
clientSecret: paymentIntent.client_secret || '',
message: 'Payment initiated successfully',
paymentIntentID: paymentIntent.id,
}
return returnData
} catch (error) {
payload.logger.error(error, 'Error initiating payment with Stripe')
throw new Error(
error instanceof Error
? error.message
: 'Unknown error initiating payment',
)
}
}
```
#### 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:
```ts
import {
PaymentAdapter,
PaymentAdapterArgs,
} from '@payloadcms/plugin-ecommerce'
import Stripe from 'stripe'
export const confirmOrder: NonNullable<PaymentAdapter>['confirmOrder'] =
async ({
data,
ordersSlug = 'orders',
req,
transactionsSlug = 'transactions',
}) => {
const payload = req.payload
const customerEmail = data.customerEmail
const paymentIntentID = data.paymentIntentID as string
const stripe = new Stripe(secretKey)
try {
// Find our existing transaction by the payment intent ID
const transactionsResults = await payload.find({
collection: transactionsSlug,
where: {
'stripe.paymentIntentID': {
equals: paymentIntentID,
},
},
})
const transaction = transactionsResults.docs[0]
// Verify the payment intent exists and retrieve it
const paymentIntent =
await stripe.paymentIntents.retrieve(paymentIntentID)
// Create the order in the database
const order = await payload.create({
collection: ordersSlug,
data: {},
})
const timestamp = new Date().toISOString()
// Update the cart to mark it as purchased, this will prevent further updates to the cart
await payload.update({
id: cartID,
collection: 'carts',
data: {
purchasedAt: timestamp,
},
})
// Update the transaction with the order ID and mark as succeeded
await payload.update({
id: transaction.id,
collection: transactionsSlug,
data: {
order: order.id,
status: 'succeeded',
},
})
return {
message: 'Payment initiated successfully',
orderID: order.id,
transactionID: transaction.id,
}
} catch (error) {
payload.logger.error(error, 'Error initiating payment with Stripe')
}
}
```
#### 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:
```ts
const groupField: GroupField = {
name: 'stripe',
type: 'group',
admin: {
condition: (data) => {
const path = 'paymentMethod'
return data?.[path] === 'stripe'
},
},
fields: [
{
name: 'customerID',
type: 'text',
label: 'Stripe Customer ID',
},
{
name: 'paymentIntentID',
type: 'text',
label: 'Stripe PaymentIntent ID',
},
],
}
```
### 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 adapter. |
## 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.