158 lines
5.1 KiB
Markdown
158 lines
5.1 KiB
Markdown
---
|
||
title: "Stripe Plugin"
|
||
aliases: [payload-stripe, plugin-stripe, stripe-payments-payload]
|
||
tags: [payloadcms, stripe, payments, ecommerce, plugin, webhooks, sync]
|
||
sources: [raw/plugins__stripe.md]
|
||
created: 2026-05-15
|
||
updated: 2026-05-15
|
||
---
|
||
|
||
# Stripe Plugin
|
||
|
||
Integrates [Stripe](https://stripe.com) billing into Payload CMS — two-way sync, webhook handling, and a proxied Stripe REST API behind Payload access control.
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
pnpm add @payloadcms/plugin-stripe
|
||
```
|
||
|
||
## Basic Config
|
||
|
||
```ts
|
||
import { stripePlugin } from '@payloadcms/plugin-stripe'
|
||
|
||
buildConfig({
|
||
plugins: [
|
||
stripePlugin({
|
||
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||
}),
|
||
],
|
||
})
|
||
```
|
||
|
||
## Options
|
||
|
||
| Option | Type | Default | Description |
|
||
|--------|------|---------|-------------|
|
||
| `stripeSecretKey` * | string | — | Stripe secret key |
|
||
| `stripeWebhooksEndpointSecret` | string | — | Webhook signing secret |
|
||
| `rest` | boolean | `false` | Open `/api/stripe/rest` proxy (dev only) |
|
||
| `webhooks` | object \| function | — | Webhook event handlers |
|
||
| `sync` | array | — | Auto-sync configs between collections and Stripe resources |
|
||
| `logs` | boolean | `false` | Log sync events to console |
|
||
|
||
## Auto-Opened Endpoints
|
||
|
||
| Endpoint | Method | Description |
|
||
|----------|--------|-------------|
|
||
| `/api/stripe/rest` | POST | Proxied Stripe REST API (behind Payload access control) |
|
||
| `/api/stripe/webhooks` | POST | Stripe webhook receiver |
|
||
|
||
## Webhooks Setup
|
||
|
||
**Dev:**
|
||
```bash
|
||
stripe login
|
||
stripe listen --forward-to localhost:3000/api/stripe/webhooks
|
||
# paste the secret into .env as STRIPE_WEBHOOKS_ENDPOINT_SECRET
|
||
```
|
||
|
||
**Production:** Create webhook in Stripe dashboard → set URL to `YOUR_DOMAIN/api/stripe/webhooks` → paste secret into `.env`.
|
||
|
||
**Handler config:**
|
||
```ts
|
||
stripePlugin({
|
||
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
|
||
webhooks: {
|
||
'customer.subscription.updated': ({ event, stripe, stripeConfig }) => {
|
||
// handle event
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### Serverless Warning
|
||
|
||
Stripe expects a `2xx` response within 10–20 seconds. The plugin processes webhooks asynchronously — in serverless environments the function instance may close before processing completes, causing duplicate events.
|
||
|
||
**Vercel fix:** install `@vercel/functions` — the plugin auto-detects it and wraps handlers in `waitUntil()`.
|
||
|
||
## Sync — Two-Way Auto-Sync
|
||
|
||
Maps Payload collection fields to Stripe resource properties. Hooks + webhook handlers are created automatically.
|
||
|
||
```ts
|
||
stripePlugin({
|
||
sync: [
|
||
{
|
||
collection: 'customers',
|
||
stripeResourceType: 'customers',
|
||
stripeResourceTypeSingular: 'customer',
|
||
fields: [
|
||
{ fieldPath: 'name', stripeProperty: 'name' },
|
||
],
|
||
},
|
||
],
|
||
})
|
||
```
|
||
|
||
**What `sync` does automatically:**
|
||
- Adds read-only `stripeID` field (Stripe-generated cross-reference)
|
||
- Adds direct link to resource on Stripe.com in admin
|
||
- Adds read-only `skipSync` flag to prevent infinite loops
|
||
- Attaches hooks: `beforeValidate: createNewInStripe`, `beforeChange: syncExistingWithStripe`, `afterDelete: deleteFromStripe`
|
||
- Handles webhooks: `STRIPE_TYPE.created/updated/deleted`
|
||
|
||
> **Limitation:** Only top-level fields supported (Stripe API constraint).
|
||
|
||
## Server-Side Usage
|
||
|
||
Prefer direct Stripe SDK on server rather than the REST proxy:
|
||
|
||
```ts
|
||
import Stripe from 'stripe'
|
||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2022-08-01' })
|
||
const customer = await stripe.customers.create({ email: data.email })
|
||
```
|
||
|
||
Or use `stripeProxy` from the plugin:
|
||
|
||
```ts
|
||
import { stripeProxy } from '@payloadcms/plugin-stripe'
|
||
const customer = await stripeProxy({
|
||
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
|
||
stripeMethod: 'customers.create',
|
||
stripeArgs: [{ email: data.email }],
|
||
})
|
||
```
|
||
|
||
## TypeScript
|
||
|
||
```ts
|
||
import { StripeConfig, StripeWebhookHandler, StripeProxy } from '@payloadcms/plugin-stripe/types'
|
||
```
|
||
|
||
## Key Takeaways
|
||
|
||
- **Hides Stripe credentials** — client-side calls go through Payload's access control, never directly to Stripe
|
||
- **REST proxy (`rest: true`) is dev-only** — opening it in production gives authenticated users full Stripe API access
|
||
- **Two-way sync requires webhooks** — set `stripeWebhooksEndpointSecret` or changes from Stripe won't propagate to Payload
|
||
- **Serverless + webhooks = `@vercel/functions`** — without it, async webhook handlers may be killed before completing
|
||
- **`sync` only works on top-level Stripe fields** — nested fields require manual hooks
|
||
- **`skipSync` flag prevents infinite loops** — sync hooks set it before triggering webhooks, webhooks check it before re-syncing
|
||
- For e-commerce, pair with [[wiki/payloadcms/ecommerce|Ecommerce Plugin]] or [[wiki/payloadcms/ecommerce-payment-adapters|Payment Adapters]]
|
||
|
||
## Related
|
||
|
||
- [[wiki/payloadcms/plugins|Plugins Overview + Official]]
|
||
- [[wiki/payloadcms/ecommerce|Ecommerce Plugin]]
|
||
- [[wiki/payloadcms/ecommerce-payment-adapters|Ecommerce — Payment Adapters]]
|
||
- [[wiki/payloadcms/access-control|Access Control]]
|
||
- [[wiki/payloadcms/hooks-collections|Collection Hooks]]
|
||
|
||
## Sources
|
||
|
||
- `raw/plugins__stripe.md`
|
||
- https://payloadcms.com/docs/plugins/stripe
|