How to Create a Payment Module Provider

In this document, you’ll learn how to create a Payment Module Provider to be used with the Payment Module.


Implementation Example#

As you implement your Payment Module Provider, it can be useful to refer to an existing provider and how it's implemeted.

If you need to refer to an existing implementation as an example, check the Stripe Payment Module Provider in the Medusa repository.


Understanding Payment Module Provider Implementation#

The Payment Module Provider handles processing payment with a third-party provirder. However, it's not responsible for managing payment concepts within Medusa, such as payment sessions or collections. These concepts are handled by the Payment Module which uses your Payment Module Provider within core operations.

For example, when the merchant captures an order's payment, the Payment Module uses the Payment Module Provider to capture the payment, the makes updates to the Payment record associated with the order. So, you only have to implement the third-party payment processing logic in your Payment Module Provider.


1. Create Module Provider Directory#

Start by creating a new directory for your module provider.

If you're creating the module provider in a Medusa application, create it under the src/modules directory. For example, src/modules/my-payment.

If you're creating the module provider in a plugin, create it under the src/providers directory. For example, src/providers/my-payment.

Note: The rest of this guide always uses the src/modules/my-payment directory as an example.

2. Create the Payment Module Provider's Service#

Create the file src/modules/my-payment/service.ts that holds the module provider's main service. It must extend the AbstractPaymentProvider class imported from @medusajs/framework/utils:

src/modules/my-payment/service.ts
1import { AbstractPaymentProvider } from "@medusajs/framework/utils"2
3type Options = {4  apiKey: string5}6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  // TODO implement methods11}12
13export default MyPaymentProviderService

constructor#

The constructor allows you to access resources from the module's container using the first parameter, and the module's options using the second parameter.

If you're creating a client or establishing a connection with a third-party service, do it in the constructor.

Note: A module's options are passed when you register it in the Medusa application.

Example

Code
1import { AbstractPaymentProvider } from "@medusajs/framework/utils"2import { Logger } from "@medusajs/framework/types"3
4type Options = {5  apiKey: string6}7
8type InjectedDependencies = {9  logger: Logger10}11
12class MyPaymentProviderService extends AbstractPaymentProvider<Options> {13  protected logger_: Logger14  protected options_: Options15  // assuming you're initializing a client16  protected client17
18  constructor(19    container: InjectedDependencies,20    options: Options21  ) {22    super(container, options)23
24    this.logger_ = container.logger25    this.options_ = options26
27    // TODO initialize your client28  }29  // ...30}31
32export default MyPaymentProviderService

Type Parameters

TConfigobjectOptional
The type of the provider's options passed as a second parameter.

Parameters

cradleRecord<string, unknown>
The module's container used to resolve resources.

identifier#

Each payment provider has a unique identifier defined in its class. The provider's ID will be stored as pp_{identifier}_{id}, where {id} is the provider's id property in the medusa-config.ts.

Example

Code
1class MyPaymentProviderService extends AbstractPaymentProvider<2  Options3> {4  static identifier = "my-payment"5  // ...6}

authorizePayment#

This method authorizes a payment session using the third-party payment provider.

During checkout, the customer may need to perform actions required by the payment provider, such as entering their card details or confirming the payment. Once that is done, the customer can place their order.

During cart-completion before placing the order, this method is used to authorize the cart's payment session with the third-party payment provider. The payment can later be captured using the capturePayment method.

Diagram showcasing authorize payment flow

When authorized successfully, a Payment is created by the Payment Module, and it's associated with the order.

Understanding data property

The data property of the method's parameter contains the PaymentSession record's data property, which was returned by the initiatePayment method.

The data property returned by this method is then stored in the created Payment record. You can store data relevant to later capture or process the payment. For example, you can store the ID of the payment in the third-party provider to reference it later.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  AuthorizePaymentInput,4  AuthorizePaymentOutput,5  PaymentSessionStatus6} from "@medusajs/framework/types"7
8class MyPaymentProviderService extends AbstractPaymentProvider<9  Options10> {11  async authorizePayment(12    input: AuthorizePaymentInput13  ): Promise<AuthorizePaymentOutput> {14    const externalId = input.data?.id15
16    // assuming you have a client that authorizes the payment17    const paymentData = await this.client.authorizePayment(externalId)18
19    return {20      data: paymentData,21      status: "authorized"22    }23  }24
25  // ...26}

Parameters

The input to authorize the payment. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<AuthorizePaymentOutput>
The status of the authorization, along with the data field about the payment. Throws in case of an error.

cancelPayment#

This method cancels a payment in the third-party payment provider. It's used when the admin user cancels an order. The order can only be canceled if the payment is not captured yet.

Understanding data property

The data property of the method's parameter contains the Payment record's data property, which was returned by the authorizePayment method.

The data property returned by this method is then stored in the Payment record. You can store data relevant for any further processing of the payment.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  PaymentProviderError,4  PaymentProviderSessionResponse,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async cancelPayment(11    input: CancelPaymentInput12  ): Promise<CancelPaymentOutput> {13    const externalId = input.data?.id14
15    // assuming you have a client that cancels the payment16    const paymentData = await this.client.cancelPayment(externalId)17    return { data: paymentData }18  }19
20  // ...21}

Parameters

The input to cancel the payment. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<CancelPaymentOutput>
The new data to store in the payment's data property, if any. Throws in case of an error.

capturePayment#

This method captures a payment using the third-party provider. In this method, use the third-party provider to capture the payment.

When an order is placed, the payment is authorized using the authorizePayment method. Then, the admin user can capture the payment, which triggers this method.

Diagram showcasing capture payment flow

This method can also be triggered by a webhook event if the getWebhookActionAndData method returns the action captured.

Understanding data property

The data property of the input parameter contains data that was previously stored in the Payment record's data property, which was returned by the authorizePayment method.

The data property returned by this method is then stored in the Payment record. You can store data relevant to later refund or process the payment. For example, you can store the ID of the payment in the third-party provider to reference it later.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  CapturePaymentInput,4  CapturePaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async capturePayment(11    input: CapturePaymentInput12  ): Promise<CapturePaymentOutput> {13    const externalId = input.data?.id14
15      // assuming you have a client that captures the payment16    const newData = await this.client.capturePayment(externalId)17    return {18      data: {19        ...newData,20        id: externalId,21      }22    }23  }24  // ...25}

Parameters

The input to capture the payment. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<CapturePaymentOutput>
The new data to store in the payment's data property. Throws in case of an error.

createAccountHolder#

optional

This method is used when creating an account holder in Medusa, allowing you to create the equivalent account in the third-party payment provider. An account holder is useful to later save payment methods, such as credit cards, for a customer in the third-party payment provider using the savePaymentMethod method.

The returned data will be stored in the account holder created in Medusa. For example, the returned id property will be stored in the account holder's external_id property.

Medusa creates an account holder when a payment session initialized for a registered customer.

Note: This is only available after Medusa v2.5.0.

Example

Code
1import { MedusaError } from "@medusajs/framework/utils"2
3class MyPaymentProviderService extends AbstractPaymentProvider<4 Options5> {6 async createAccountHolder({ context, data }: CreateAccountHolderInput) {7  const { account_holder, customer } = context8
9  if (account_holder?.data?.id) {10    return { id: account_holder.data.id as string }11  }12
13  if (!customer) {14    throw new MedusaError(15      MedusaError.Types.INVALID_DATA,16      "Missing customer data."17    )18  }19
20  // assuming you have a client that creates the account holder21  const providerAccountHolder = await this.client.createAccountHolder({22    email: customer.email,23   ...data24  })25
26  return {27    id: providerAccountHolder.id,28    data: providerAccountHolder as unknown as Record<string, unknown>29  }30}

Parameters

Input data including the details of the account holder to create.

Returns

The result of creating the account holder. If an error occurs, throw it.

deleteAccountHolder#

optional

This method is used when an account holder is deleted in Medusa, allowing you to also delete the equivalent account holder in the third-party payment provider.

Note: This is only available after Medusa v2.5.0.

Example

Code
1import { MedusaError } from "@medusajs/framework/utils"2
3class MyPaymentProviderService extends AbstractPaymentProvider<4 Options5> {6  async deleteAccountHolder({ context }: DeleteAccountHolderInput) {7    const { account_holder } = context8    const accountHolderId = account_holder?.data?.id as string | undefined9    if (!accountHolderId) {10      throw new MedusaError(11        MedusaError.Types.INVALID_DATA,12        "Missing account holder ID."13      )14    }15
16    // assuming you have a client that deletes the account holder17    await this.client.deleteAccountHolder({18      id: accountHolderId19    })20
21    return {}22  }23}

Parameters

Input data including the details of the account holder to delete.

Returns

The result of deleting the account holder. If an error occurs, throw it.

deletePayment#

This method deletes a payment session in the third-party payment provider.

When a customer chooses a payment method during checkout, then chooses a different one, this method is triggered to delete the previous payment session.

If your provider doesn't support deleting a payment session, you can just return an empty object or an object that contains the same received data property.

Diagram showcasing delete payment flow

Understanding data property

The data property of the method's parameter contains the PaymentSession record's data property, which was returned by the initiatePayment method.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  DeletePaymentInput,4  DeletePaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async deletePayment(11    input: DeletePaymentInput12  ): Promise<DeletePaymentOutput> {13    const externalId = input.data?.id14
15    // assuming you have a client that cancels the payment16    await this.client.cancelPayment(externalId)17    return {18      data: input.data19    }20  }21
22  // ...23}

Parameters

The input to delete the payment session. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<DeletePaymentOutput>
The new data to store in the payment's data property, if any. Throws in case of an error.

getPaymentStatus#

This method gets the status of a payment session based on the status in the third-party integration.

Example

Code
1// other imports...2import {3  GetPaymentStatusInput,4  GetPaymentStatusOutput,5  PaymentSessionStatus6} from "@medusajs/framework/types"7
8class MyPaymentProviderService extends AbstractPaymentProvider<9  Options10> {11  async getPaymentStatus(12    input: GetPaymentStatusInput13  ): Promise<GetPaymentStatusOutput> {14    const externalId = input.data?.id15
16    // assuming you have a client that retrieves the payment status17    const status = await this.client.getStatus(externalId)18
19    switch (status) {20      case "requires_capture":21          return {status: "authorized"}22        case "success":23          return {status: "captured"}24        case "canceled":25          return {status: "canceled"}26        default:27          return {status: "pending"}28     }29  }30
31  // ...32}

Parameters

The input to get the payment status. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<GetPaymentStatusOutput>
The payment session's status. It can also return additional data from the payment provider.

getWebhookActionAndData#

This method is executed when a webhook event is received from the third-party payment provider. Medusa uses the data returned by this method to perform actions in the Medusa application, such as completing the associated cart if the payment was authorized successfully.

Learn more in the Webhook Events documentation.

Example

Code
1// other imports...2import {3  BigNumber4} from "@medusajs/framework/utils"5import {6  ProviderWebhookPayload,7  WebhookActionResult8} from "@medusajs/framework/types"9
10class MyPaymentProviderService extends AbstractPaymentProvider<11  Options12> {13  async getWebhookActionAndData(14    payload: ProviderWebhookPayload["payload"]15  ): Promise<WebhookActionResult> {16    const {17      data,18      rawData,19      headers20    } = payload21
22    try {23      switch(data.event_type) {24        case "authorized_amount":25          return {26            action: "authorized",27            data: {28              // assuming the session_id is stored in the metadata of the payment29              // in the third-party provider30              session_id: (data.metadata as Record<string, any>).session_id,31              amount: new BigNumber(data.amount as number)32            }33          }34        case "success":35          return {36            action: "captured",37            data: {38              // assuming the session_id is stored in the metadata of the payment39              // in the third-party provider40              session_id: (data.metadata as Record<string, any>).session_id,41              amount: new BigNumber(data.amount as number)42            }43          }44        default:45          return {46            action: "not_supported",47            data: {48              session_id: "",49              amount: new BigNumber(0)50            }51          }52      }53    } catch (e) {54      return {55        action: "failed",56        data: {57          // assuming the session_id is stored in the metadata of the payment58          // in the third-party provider59          session_id: (data.metadata as Record<string, any>).session_id,60          amount: new BigNumber(data.amount as number)61        }62      }63    }64  }65
66  // ...67}

Parameters

dataobject
The webhook event's data

Returns

PromisePromise<WebhookActionResult>
The webhook result. If the action's value is captured, the payment is captured within Medusa as well. If the action's value is authorized, the associated payment session is authorized within Medusa and the associated cart will be completed to create an order.

initiatePayment#

This method initializes a payment session with the third-party payment provider.

When a customer chooses a payment method during checkout, this method is triggered to perform any initialization action with the third-party provider, such as creating a payment session.

Diagram showcasing initiate payment flow

Understanding data property

The data property returned by this method will be stored in the created PaymentSession record. You can store data relevant to later authorize or process the payment. For example, you can store the ID of the payment session in the third-party provider to reference it later.

The data property is also available to storefronts, allowing you to store data necessary for the storefront to integrate the payment provider in the checkout flow. For example, you can store the client token to use with the payment provider's SDK.

Note: This also means you shouldn't store sensitive data and tokens in the data property, as it's publicly accessible.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  InitiatePaymentInput,4  InitiatePaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async initiatePayment(11    input: InitiatePaymentInput12  ): Promise<InitiatePaymentOutput> {13    const {14      amount,15      currency_code,16      context: customerDetails17    } = input18
19    // assuming you have a client that initializes the payment20    const response = await this.client.init(21      amount, currency_code, customerDetails22    )23
24    return {25      id: response.id,26      data: response,27    }28  }29
30  // ...31}

Parameters

The input to create the payment session.

Returns

PromisePromise<InitiatePaymentOutput>
The new data to store in the payment's data property. Throws in case of an error.

listPaymentMethods#

optional

This method is used to retrieve the list of saved payment methods for an account holder in the third-party payment provider. A payment provider that supports saving payment methods must implement this method.

Note: This is only available after Medusa v2.5.0.

Example

Code
1import { MedusaError } from "@medusajs/framework/utils"2
3class MyPaymentProviderService extends AbstractPaymentProvider<4  Options5> {6  async listPaymentMethods({ context }: ListPaymentMethodsInput) {7    const { account_holder } = context8    const accountHolderId = account_holder?.data?.id as string | undefined9
10    if (!accountHolderId) {11      throw new MedusaError(12        MedusaError.Types.INVALID_DATA,13        "Missing account holder ID."14      )15    }16
17   // assuming you have a client that lists the payment methods18   const paymentMethods = await this.client.listPaymentMethods({19     customer_id: accountHolderId20   })21
22   return paymentMethods.map((pm) => ({23     id: pm.id,24     data: pm as unknown as Record<string, unknown>25   }))26 }27}

Parameters

Input data including the details of the account holder to list payment methods for.

Returns

PromisePromise<ListPaymentMethodsOutput>
The list of payment methods saved for the account holder. If an error occurs, throw it.

refundPayment#

This method refunds an amount using the third-party payment provider. This method is triggered when the admin user refunds a payment of an order.

Understanding data property

The data property of the method's parameter contains the Payment record's data property, which was returned by the capturePayment or refundPayment method.

The data property returned by this method is then stored in the Payment record. You can store data relevant to later refund or process the payment. For example, you can store the ID of the payment in the third-party provider to reference it later.

Note: A payment may be refunded multiple times with different amounts. In this case, the data property of the input parameter contains the data from the last refund.

Diagram showcasing data flow between methods

Example

Code
1// other imports...2import {3  RefundPaymentInput,4  RefundPaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async refundPayment(11    input: RefundPaymentInput12  ): Promise<RefundPaymentOutput> {13    const externalId = input.data?.id14
15    // assuming you have a client that refunds the payment16    const newData = await this.client.refund(17        externalId,18        input.amount19      )20
21    return {22      data: input.data,23    }24  }25  // ...26}

Parameters

The input to refund the payment. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<RefundPaymentOutput>
The new data to store in the payment's data property, or an error object.

retrievePayment#

This method retrieves the payment's data from the third-party payment provider.

Example

Code
1// other imports...2import {3  RetrievePaymentInput,4  RetrievePaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async retrievePayment(11    input: RetrievePaymentInput12  ): Promise<RetrievePaymentOutput> {13    const externalId = input.data?.id14
15    // assuming you have a client that retrieves the payment16    return await this.client.retrieve(externalId)17  }18  // ...19}

Parameters

The input to retrieve the payment. The data field should contain the data from the payment provider when the payment was created.

Returns

PromisePromise<RetrievePaymentOutput>
The payment's data as found in the the payment provider.

savePaymentMethod#

optional

This method is used to save a customer's payment method, such as a credit card, in the third-party payment provider. A payment provider that supports saving payment methods must implement this method.

Note: This is only available after Medusa v2.5.0.

Example

Code
1import { MedusaError } from "@medusajs/framework/utils"2
3class MyPaymentProviderService extends AbstractPaymentProvider<4  Options5> {6  async savePaymentMethod({ context, data }: SavePaymentMethodInput) {   *7    const accountHolderId = context?.account_holder?.data?.id as8      | string9      | undefined10
11    if (!accountHolderId) {12      throw new MedusaError(13        MedusaError.Types.INVALID_DATA,14        "Missing account holder ID."15      )16    }17
18   // assuming you have a client that saves the payment method19   const paymentMethod = await this.client.savePaymentMethod({20     customer_id: accountHolderId,21     ...data22   })23
24  return {25    id: paymentMethod.id,26    data: paymentMethod as unknown as Record<string, unknown>27  }28 }29}

Parameters

The details of the payment method to save.

Returns

PromisePromise<SavePaymentMethodOutput>
The result of saving the payment method. If an error occurs, throw it.

updateAccountHolder#

optional

This method is used when updating an account holder in Medusa, allowing you to update the equivalent account in the third-party payment provider.

The returned data will be stored in the account holder created in Medusa. For example, the returned id property will be stored in the account holder's external_id property.

Note: This is only available after Medusa v2.5.1.

Example

Code
1import { MedusaError } from "@medusajs/framework/utils"2
3class MyPaymentProviderService extends AbstractPaymentProvider<4 Options5> {6 async updateAccountHolder({ context, data }: UpdateAccountHolderInput) {7  const { account_holder, customer } = context8
9  if (!account_holder?.data?.id) {10    throw new MedusaError(11      MedusaError.Types.INVALID_DATA,12      "Missing account holder ID."13    )14  }15
16  // assuming you have a client that updates the account holder17  const providerAccountHolder = await this.client.updateAccountHolder({18    id: account_holder.data.id,19   ...data20  })21
22  return {23    id: providerAccountHolder.id,24    data: providerAccountHolder as unknown as Record<string, unknown>25  }26}

Parameters

Input data including the details of the account holder to update.

Returns

The result of updating the account holder. If an error occurs, throw it.

updatePayment#

This method updates a payment in the third-party service that was previously initiated with the initiatePayment method.

Example

Code
1// other imports...2import {3  UpdatePaymentInput,4  UpdatePaymentOutput,5} from "@medusajs/framework/types"6
7class MyPaymentProviderService extends AbstractPaymentProvider<8  Options9> {10  async updatePayment(11    input: UpdatePaymentInput12  ): Promise<UpdatePaymentOutput> {13    const { amount, currency_code, context } = input14    const externalId = input.data?.id15
16    // Validate context.customer17    if (!context || !context.customer) {18      throw new Error("Context must include a valid customer.");19    }20
21    // assuming you have a client that updates the payment22    const response = await this.client.update(23      externalId,24        {25          amount,26          currency_code,27          context.customer28        }29      )30
31    return response32  }33
34  // ...35}

Parameters

The input to update the payment. The data field should contain the data from the payment provider. when the payment was created.

Returns

PromisePromise<UpdatePaymentOutput>
The new data to store in the payment's data property. Throws in case of an error.

validateOptions#

This method validates the options of the provider set in medusa-config.ts. Implementing this method is optional, but it's useful to ensure that the required options are passed to the provider, or if you have any custom validation logic.

If the options aren't valid, throw an error.

Example

Code
1class MyPaymentProviderService extends AbstractPaymentProvider<Options> {2  static validateOptions(options: Record<any, any>) {3    if (!options.apiKey) {4      throw new MedusaError(5        MedusaError.Types.INVALID_DATA,6        "API key is required in the provider's options."7      )8    }9  }10}

Parameters

optionsRecord<any, any>
The provider's options passed in medusa-config.ts.

3. Create Module Provider Definition File#

Create the file src/modules/my-payment/index.ts with the following content:

src/modules/my-payment/index.ts
1import MyPaymentProviderService from "./service"2import { 3  ModuleProvider, 4  Modules5} from "@medusajs/framework/utils"6
7export default ModuleProvider(Modules.PAYMENT, {8  services: [MyPaymentProviderService],9})

This exports the module provider's definition, indicating that the MyPaymentProviderService is the module provider's service.

Tip: A payment module provider can have export multiple provider services, where each are registered as a separate payment provider.

4. Use Module Provider#

To use your Payment Module Provider, add it to the providers array of the Payment Module in medusa-config.ts:

medusa-config.ts
1module.exports = defineConfig({2  // ...3  modules: [4    {5      resolve: "@medusajs/medusa/payment",6      options: {7        providers: [8          {9            // if module provider is in a plugin, use `plugin-name/providers/my-payment`10            resolve: "./src/modules/my-payment",11            id: "my-payment",12            options: {13              // provider options...14              apiKey: "..."15            }16          }17        ]18      }19    }20  ]21})

5. Test it Out#

Before you use your Payment Module Provider, enable it in a region using the Medusa Admin.

Then, go through checkout to place an order. Your Payment Module Provider is used to authorize the payment.


Useful Guides#

Was this page helpful?
Ask Anything
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