payloadcms/payload · error · Error

Cart ID not found in the PaymentIntent metadata

Error message

Cart ID not found in the PaymentIntent metadata

What it means

Thrown by Stripe confirmOrder when paymentIntent.metadata.cartID is missing. The adapter stores cartID in PaymentIntent metadata during initiatePayment so that, on confirm, it can mark the source cart as purchased. A missing cartID means the metadata contract was broken — the intent was created without the plugin's normal flow, or metadata was stripped.

Source

Thrown at packages/plugin-ecommerce/src/payments/adapters/stripe/confirmOrder.ts:93

      }

      const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentID)

      if (paymentIntent.status !== 'succeeded') {
        throw new Error(`Payment not completed.`)
      }

      const cartID = paymentIntent.metadata.cartID
      const cartItemsSnapshot = paymentIntent.metadata.cartItemsSnapshot
        ? JSON.parse(paymentIntent.metadata.cartItemsSnapshot)
        : undefined

      const shippingAddress = paymentIntent.metadata.shippingAddress
        ? JSON.parse(paymentIntent.metadata.shippingAddress)
        : undefined

      if (!cartID) {
        throw new Error('Cart ID not found in the PaymentIntent metadata')
      }

      if (!cartItemsSnapshot || !Array.isArray(cartItemsSnapshot)) {
        throw new Error('Cart items snapshot not found or invalid in the PaymentIntent metadata')
      }

      const order = await payload.create({
        collection: ordersSlug,
        data: {
          amount: paymentIntent.amount,
          currency: paymentIntent.currency.toUpperCase(),
          ...(req.user ? { customer: req.user.id } : { customerEmail }),
          items: cartItemsSnapshot,
          shippingAddress,
          status: 'processing',
          transactions: [transaction.id],
        },
        req,

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Always create intents through initiatePayment so metadata.cartID is set from cart.id.
  2. If you have a custom flow, set metadata: { cartID, cartItemsSnapshot, shippingAddress } when creating the PaymentIntent.
  3. For test intents created in the dashboard, populate the cartID metadata field manually before confirming.
  4. Verify metadata on the intent via stripe.paymentIntents.retrieve to confirm cartID is present.

Example fix

// before: custom intent creation omits metadata
const intent = await stripe.paymentIntents.create({ amount, currency })
// after: include the plugin's expected metadata keys
const intent = await stripe.paymentIntents.create({
  amount,
  currency,
  metadata: {
    cartID: cart.id,
    cartItemsSnapshot: JSON.stringify(items),
    shippingAddress: JSON.stringify(shippingAddress),
  },
})
Defensive patterns

Strategy: validation

Validate before calling

import Stripe from 'stripe'

async function ensureCartMetadata(stripe: Stripe, paymentIntentID: string): Promise<{ cartID: string }> {
  const intent = await stripe.paymentIntents.retrieve(paymentIntentID)
  const cartID = intent.metadata?.cartID
  if (typeof cartID !== 'string' || cartID.length === 0) {
    throw new Error('PaymentIntent is missing metadata.cartID; create it via initiatePayment')
  }
  return { cartID }
}

Type guard

export function hasCartMetadata(intent: { metadata?: Record<string, unknown> }): intent is { metadata: { cartID: string } } {
  return typeof intent.metadata?.cartID === 'string' && intent.metadata.cartID.length > 0
}

Try / catch

try {
  await payments.confirmOrder({ data: { paymentIntentID } })
} catch (err) {
  if (err instanceof Error && /Cart ID not found in the PaymentIntent metadata/.test(err.message)) {
    // intent created outside the plugin — record and route to manual handling
    return { ok: false, reason: 'missing-cart-metadata', requiresManualReview: true }
  }
  throw err
}

Prevention

When it happens

Trigger: confirmOrder is invoked on an intent whose metadata lacks cartID. This happens if the intent was created outside initiatePayment, if a custom initiate path omitted the cartID metadata, or if metadata was overwritten/lost in a Stripe migration or manual edit.

Common situations: Custom Stripe integration that reuses confirmOrder but not initiatePayment; intents created in the Stripe dashboard for testing; a fork of the plugin that dropped the metadata.cartID line; Stripe metadata size limits truncating large carts (metadata values are capped at 500 chars and 50 keys).

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/92668a3b6b24445a. Report an issue: GitHub.