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.
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 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 likeexternal_idanddatain the createdAccountHolderrecord.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.
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#
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:
- It tries to create the payment session with the customer's existing account holder using the
tryCreatePaymentSessionStep. - If the payment provider reports that the account holder doesn't exist, the workflow uses the
replacePaymentAccountHolderStepto create a new account holder in the provider. - 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:
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 from 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:
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.