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.user is an API-key doc, user?.email is undefined, so customerEmail stays ''.
user is truthy → the else branch that reads data.customerEmail is skipped.
- The (empty)
customerEmail is 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
customerEmail pre-check throws because the endpoint forwarded an empty string (symptom 1).
- Even if
customerEmail were non-empty and Stripe succeeded, the transaction write would set customer: req.user.id — but req.user.id is an api-keys doc id, not a valid customers-collection id. The transactions.customer relationship validation then fails, and customerEmail is 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, onInit seeds:
- an
api-keys auth doc with a known key 00000000-0000-0000-0000-000000000001
- a published product with
priceInUSD: 1000
- a cart containing that product
-
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 customerEmail in 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 customerEmail from the body, initiates payment, and writes a transaction with customerEmail (not customer).
-
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 customerEmail because it read req.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
Describe the Bug
The plugin's
initiatePaymentendpoint and the built-in Stripe payment adapter both readreq.userdirectly 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-machineapi-keyscollection distinct from the customers collection),req.useris set to that API-key doc — which has noemailfield and is not a valid customers-collection reference.Symptom 1 —
initiatePaymentendpointSource:
packages/plugin-ecommerce/src/endpoints/initiatePayment.tsreq.useris an API-key doc,user?.emailisundefined, socustomerEmailstays''.useris truthy → theelsebranch that readsdata.customerEmailis skipped.customerEmailis forwarded to the payment adapter, where it blows up validation.Symptom 2 — Stripe adapter
Source:
packages/plugin-ecommerce/src/payments/adapters/stripe/initiatePayment.tsTwo problems, same root cause:
customerEmailpre-check throws because the endpoint forwarded an empty string (symptom 1).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.userasundefined/ guest: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:api-keysauth doc with a known key00000000-0000-0000-0000-000000000001priceInUSD: 1000Find 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):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:The endpoint silently dropped the caller-supplied
customerEmailbecause it readreq.user.email(empty) instead.Which area(s) are affected?
plugin: ecommerce
Environment Info