payloadcms/payload · error · APIError

Failed to delete Stripe document with ID: '${doc.stripeID}':

Error message

Failed to delete Stripe document with ID: '${doc.stripeID}': ${msg}

What it means

Thrown by the Stripe plugin's `deleteFromStripe` after-delete hook when either `stripe.[stripeResourceType].retrieve(stripeID)` or `.del(stripeID)` rejects. The original Stripe SDK error message is appended so the cause is visible. Note: a missing Stripe resource is NOT an error — the hook logs and skips — this throw is for genuine failures (auth, network, invalid ID format).

Source

Thrown at packages/plugin-stripe/src/hooks/deleteFromStripe.ts:63

        const found = await stripe?.[syncConfig.stripeResourceType]?.retrieve(doc.stripeID)

        if (found) {
          await stripe?.[syncConfig.stripeResourceType]?.del(doc.stripeID)
          if (logs) {
            payload.logger.info(
              `✅ Successfully deleted Stripe document with ID: '${doc.stripeID}'.`,
            )
          }
        } else {
          if (logs) {
            payload.logger.info(
              `- Stripe document with ID: '${doc.stripeID}' not found, skipping...`,
            )
          }
        }
      } catch (error: unknown) {
        const msg = error instanceof Error ? error.message : error
        throw new APIError(`Failed to delete Stripe document with ID: '${doc.stripeID}': ${msg}`)
      }
    }
  }
}

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Read the `${msg}` suffix — it names the Stripe error (e.g. 'Cannot delete customer with active subscription')
  2. Resolve the upstream Stripe constraint (close subscriptions, etc.) before deleting the Payload doc
  3. Verify `stripeSecretKey` is valid for the account that owns the resource
  4. Confirm `stripeResourceType` supports `.retrieve` and `.del`

Example fix

// before — customer still has subscription, Stripe rejects delete
await payload.delete({ collection: 'users', id })
// after — cancel subscriptions first
await stripe.subscriptions.cancel(sub.id)
await payload.delete({ collection: 'users', id })
Defensive patterns

Strategy: try-catch

Validate before calling

// Before deleting, confirm the Stripe resource can be deleted (e.g. customer has no subs)
if (stripeResourceType === 'customers') {
  const subs = await stripe.customers.listSubscriptions(stripeID)
  if (subs.data.length) throw new Error('customer has active subscriptions — cancel first')
}

Type guard

import { APIError } from 'payload'
function isStripeDeleteError(e: unknown): e is APIError {
  return e instanceof APIError && /^Failed to delete Stripe document with ID:/.test(e.message)
}

Try / catch

import { APIError } from 'payload'
try {
  await payload.delete({ collection: 'users', id })
} catch (e) {
  if (e instanceof APIError && /Failed to delete Stripe document/.test(e.message)) {
    // suffix holds the Stripe reason; resolve the upstream constraint (cancel subs, etc.)
  }
  throw e
}

Prevention

When it happens

Trigger: Deleting a Payload document whose `stripeID` points to a resource Stripe refuses to delete (e.g. a customer with an active subscription); `stripeSecretKey` invalid; `stripeResourceType` does not support `.retrieve`/`.del`; network failure mid-call.

Common situations: `stripeSecretKey` rotated but plugin config not updated; deleting a Stripe customer that still has subscriptions/invoices (Stripe rejects); `stripeResourceType` misconfigured to a non-deletable resource; stale `stripeID` left over from a deleted Stripe resource that the retrieve call cannot reach due to permissions.

Related errors


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