payloadcms / payloadcms/payload
bug: ecommerce plugin treats non-customer (API key) auth users as customers in initiatePayment + Stripe adapter
@paulpopus is already working on this.
Since May 4, 2026.
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Description
Describe the Bug
The plugin's initiatePayment endpoint and the built-in Stripe payment adapter both read req.user directly and assume it is a customer doc. When a request authenticates to the CMS with an API key from a separate auth collection (e.g. a machine-to-machine api-keys collection distinct from the customers collection), req.user is set to that API-key doc — which has no email field and is not a valid customers-collection reference.
Symptom 1 — initiatePayment endpoint
Source: packages/plugin-ecommerce/src/endpoints/initiatePayment.ts
const user = req.user
let customerEmail = user?.email ?? ''
if (user) {
// ...cart handling...
} else {
if (data?.customerEmail && typeof data.customerEmail === 'string') {
customerEmail = data.customerEmail
} else {
return Response.json({ message: 'A customer email is required to make a purchase.' }, { status: 400 })
}
}
- When
req.useris an API-key doc,user?.emailisundefined, socustomerEmailstays''. useris truthy → theelsebranch that readsdata.customerEmailis skipped.- The (empty)
customerEmailis forwarded to the payment adapter, where it blows up validation.
Symptom 2 — Stripe adapter
Source: packages/plugin-ecommerce/src/payments/adapters/stripe/initiatePayment.ts
if (!customerEmail || typeof customerEmail !== 'string') {
throw new Error('A valid customer email is required to make a purchase.')
}
// ...later, creating the transaction doc:
const transaction = await payload.create({
collection: transactionsSlug,
data: {
...(req.user ? { customer: req.user.id } : { customerEmail }),
// ...
},
req,
})
Two problems, same root cause:
- The
customerEmailpre-check throws because the endpoint forwarded an empty string (symptom 1). - Even if
customerEmailwere non-empty and Stripe succeeded, the transaction write would setcustomer: req.user.id— butreq.user.idis an api-keys doc id, not a validcustomers-collection id. Thetransactions.customerrelationship validation then fails, andcustomerEmailis never set on the transaction either.
Suggested root-cause fix
Both call sites should treat a non-customer req.user as undefined / guest:
const isCustomer = req.user?.collection === customersSlug
const user = isCustomer ? req.user : undefined
And in the Stripe adapter transaction write:
data: {
- ...(req.user ? { customer: req.user.id } : { customerEmail }),
+ ...(req.user?.collection === customersSlug
+ ? { customer: req.user.id }
+ : { customerEmail }),
...
},
Link to the code that reproduces this issue
https://github.com/jhb-dev/payload-ecommerce-initiate-payment-api-key-auth
Reproduction Steps
-
Clone the reproduction repository and run the development server.
-
On first boot,
onInitseeds:- an
api-keysauth doc with a known key00000000-0000-0000-0000-000000000001 - a published product with
priceInUSD: 1000 - a cart containing that product
- an
-
Find the seeded cart ID via
GET /api/carts. -
POST to the plugin's Stripe-initiate endpoint, authenticating with the api-keys key and passing a
customerEmailin the body (as a web app would for guest checkout):curl -X POST http://localhost:3000/api/payments/stripe/initiate \ -H "Content-Type: application/json" \ -H "Authorization: api-keys API-Key 00000000-0000-0000-0000-000000000001" \ -d '{"cartID":"<seeded-cart-id>","customerEmail":"guest@example.com"}' -
Expected: the endpoint treats a non-customer user as a guest, uses
customerEmailfrom the body, initiates payment, and writes a transaction withcustomerEmail(notcustomer). -
Actual: HTTP 500
{"message":"Error initiating payment."}, with server log:[ERROR]: Error initiating payment. err: { "type": "Error", "message": "A valid customer email is required to make a purchase.", "stack": Error: A valid customer email is required to make a purchase. at Object.eval [as initiatePayment] (@payloadcms/plugin-ecommerce/dist/payments/adapters/stripe/initiatePayment.js:26:19) at eval (@payloadcms/plugin-ecommerce/dist/endpoints/initiatePayment.js:209:57)The endpoint silently dropped the caller-supplied
customerEmailbecause it readreq.user.email(empty) instead.
Which area(s) are affected?
plugin: ecommerce
Environment Info
Binaries:
Node: 24.3.0
npm: 11.4.2
pnpm: 10.33.0
Relevant Packages:
payload: 3.84.0
@payloadcms/plugin-ecommerce: 3.84.0
next: 15.4.8
@payloadcms/db-mongodb: 3.84.0
@payloadcms/graphql: 3.84.0
@payloadcms/next/utilities: 3.84.0
@payloadcms/richtext-lexical: 3.84.0
@payloadcms/translations: 3.84.0
@payloadcms/ui/shared: 3.84.0
react: 19.2.1
react-dom: 19.2.1
Operating System:
Platform: darwin
Arch: arm64
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.