Account Holders and Saved Payment Methods

In this documentation, you'll learn about account holders, and how they're used to save payment methods in third-party payment providers.

Note: Account holders are available starting from Medusa v2.5.0.

What's an Account Holder?#

An account holder represents a customer that can have saved payment methods in a third-party service. It's represented by the AccountHolder data model.

It holds fields retrieved from the third-party provider, such as:

  • external_id: The ID of the equivalent customer or account holder in the third-party provider.
  • data: Data returned by the payment provider when the account holder is created.

A payment provider that supports saving payment methods for customers would create the equivalent of an account holder in the third-party provider. Then, whenever a payment method is saved, it would be saved under the account holder in the third-party provider.

Relation between Account Holder and Customer#

The Medusa application creates a link between the Customer data model of the Customer Module and the AccountHolder data model of the Payment Module.

This link indicates that a customer can have more than one account holder, each representing saved payment methods in different payment providers.

While a customer can have many account holders, each account holder belongs to only one customer. If you try to link an account holder that already belongs to another customer, Medusa throws a Cannot create multiple links between 'customer' and 'payment' error.

So, to move an account holder to a different customer, such as when transferring saved payment methods from a guest customer to a newly registered customer, remove the existing link first, then create the new link. This is a move, not an addition: the original customer loses access to that account holder's saved payment methods once you remove its link.

Learn more about this link in the Link to Other Modules guide.


Save Payment Methods#

If a payment provider supports saving payment methods for a customer, they must implement the following methods:

  • createAccountHolder: Creates an account holder in the payment provider. The Payment Module uses this method before creating the account holder in Medusa, and uses the returned data to set fields like external_id and data in the created AccountHolder record.
  • deleteAccountHolder: Deletes an account holder in the payment provider. The Payment Module uses this method when an account holder is deleted in Medusa.
  • savePaymentMethod: Saves a payment method for an account holder in the payment provider.
  • deletePaymentMethod: Deletes a saved payment method from the payment provider. This allows customers to remove their saved payment methods.
  • listPaymentMethods: Lists saved payment methods in the third-party service for an account holder. This is useful when displaying the customer's saved payment methods in the storefront.

Learn more about implementing these methods in the Create Payment Provider guide.


Account Holder in Medusa Payment Flows#

In the Medusa application, when a payment session is created for a registered customer, the Medusa application uses the Payment Module to create an account holder for the customer.

Consequently, the Payment Module uses the payment provider to create an account holder in the third-party service, then creates the account holder in Medusa.

This flow is only supported if the chosen payment provider has implemented the necessary save payment methods.


Account Holder Idempotency#

Account holder creation is idempotent per customer. When the Payment Module calls createAccountHolder on the payment provider, it passes idempotency_key: customer.id as part of the provider context. This means that if the same customer's account holder is created more than once, the payment provider returns the same result rather than creating a duplicate.

Warning: Do not compensate account holder creation with a provider-side delete in your custom workflows. If you call deleteAccountHolder in a compensation step, a subsequent retry will ask the provider to create the account holder again. However, the provider may replay the original creation response for the same idempotency key, returning the already-deleted object. This results in an account holder in Medusa that points to a deleted provider-side customer, causing saved payment method flows to fail until the provider's idempotency window expires.

The built-in createPaymentSessionsWorkflow accounts for this behavior by calling the createPaymentAccountHolderStep with noCompensation: true. This means the step is not compensated when the workflow rolls back. If the account holder was already linked to the customer before the workflow ran, the step returns the existing account holder and skips provider-side creation entirely.

If you build a custom workflow that creates account holders, use the same noCompensation: true config on createPaymentAccountHolderStep, or omit a compensation function that calls deleteAccountHolder on the provider.


Replace Missing Account Holders#

Note: Replacing missing account holders is available since Medusa v2.22.7.

An account holder in Medusa can point to a customer that no longer exists in the third-party payment provider. For example, this happens when you copy your database from an environment that uses a different payment provider account, such as copying a production database to a staging environment that uses a Stripe test account.

Replacement in the Payment Session Flow#

The createPaymentSessionsWorkflow handles missing account holders automatically:

  1. It tries to create the payment session with the customer's existing account holder using the tryCreatePaymentSessionStep.
  2. If the payment provider reports that the account holder doesn't exist, the workflow uses the replacePaymentAccountHolderStep to create a new account holder in the provider.
  3. The workflow then creates the payment session again with the replacement account holder.

The replacement keeps the account holder's ID in Medusa, so its link to the customer stays the same. However, payment methods saved under the old provider-side customer aren't available anymore.

Replace an Account Holder Manually#

You can also replace an account holder using the replaceAccountHolder method of the Payment Module's service. The method creates a new account holder in the payment provider, updates the external_id, email, and data fields of the existing AccountHolder record, then deletes the old account holder in the provider.

For example:

Code
const accountHolder =
  await paymentModuleService.replaceAccountHolder({
    id: "acchld_123",
    provider_id: "pp_stripe_stripe",
    context: {
      customer: {
        id: "cus_123",
        email: "jane@example.com",
      },
    },
  })

The method throws an error if the payment provider doesn't support account holders.

In a workflow, use the replacePaymentAccountHolderStep from @medusajs/medusa/core-flows instead, which accepts the same input.

Support Replacement in Custom Payment Providers#

For Medusa to detect a missing account holder, your custom payment provider must throw a MedusaError with the MedusaError.Codes.ACCOUNT_HOLDER_NOT_FOUND code when the third-party service reports that the customer doesn't exist. For example, in the provider's initiatePayment method:

Code
import { MedusaError } from "@medusajs/framework/utils"

// in the provider's service...
if (isCustomerMissingError(error)) {
  throw new MedusaError(
    MedusaError.Types.NOT_FOUND,
    error.message,
    MedusaError.Codes.ACCOUNT_HOLDER_NOT_FOUND
  )
}

Where isCustomerMissingError is a placeholder for your own check of the third-party service's error.

Since replacing an account holder deletes the old one in the provider, your provider's deleteAccountHolder method should also succeed without errors if the customer doesn't exist in the third-party service anymore. The Stripe and Medusa Payments providers already implement both behaviors.

Was this page helpful?
Ask Bloom
For assistance in your development, use Claude Code Plugins or Docs MCP server in Cursor, VSCode, etc...FAQ
What is Medusa?
How can I create a module?
How can I create a data model?
How do I create a workflow?
How can I extend a data model in the Product Module?
Recipes
How do I build a marketplace with Medusa?
How do I build digital products with Medusa?
How do I build subscription-based purchases with Medusa?
What other recipes are available in the Medusa documentation?
Chat is cleared on refresh
Line break
⇧↵