payloadcms / payloadcms/payload

bug: ecommerce plugin treats non-customer (API key) auth users as customers in initiatePayment + Stripe adapter

Open
#16,377 1 comment 0 reactions 1 assignee View on GitHub

@paulpopus is already working on this.

Since May 4, 2026.

created-by: Contributor plugin: ecommerce
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.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:

  1. The customerEmail pre-check throws because the endpoint forwarded an empty string (symptom 1).
  2. 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
  1. Clone the reproduction repository and run the development server.

  2. 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
  3. Find the seeded cart ID via GET /api/carts.

  4. 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"}'
    
  5. 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).

  6. 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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.