Payment Gateway Integration in Medusa.js
Medusa.js is open-source headless e-commerce platform on Node.js. Payment providers implemented as plugins via standard AbstractPaymentProvider interface. Official integrations exist for Stripe, PayPal, Klarna. Custom provider written in 2–4 working days.
Provider Architecture
Each provider implements set of methods that Medusa calls at specific checkout flow points:
-
initiatePayment— create payment session/link -
authorizePayment— confirm authorization (after redirect) -
capturePayment— charge funds -
refundPayment— perform refund -
cancelPayment— cancel payment -
retrievePayment— get current status -
getPaymentStatus— map provider status to Medusa status
Basic Provider
import {
AbstractPaymentProvider,
PaymentProviderError,
PaymentProviderSessionResponse,
PaymentSessionStatus,
CreatePaymentProviderSession,
UpdatePaymentProviderSession,
} from '@medusajs/framework/utils';
class MyPayProvider extends AbstractPaymentProvider<MyPayOptions> {
static identifier = 'mypay';
private client: MyPayClient;
constructor(container: unknown, options: MyPayOptions) {
super(container, options);
this.client = new MyPayClient(options.apiKey, options.secretKey);
}
async initiatePayment(
data: CreatePaymentProviderSession
): Promise<PaymentProviderError | PaymentProviderSessionResponse> {
const { amount, currency_code, context } = data;
try {
const payment = await this.client.createPayment({
amount: Math.round(amount),
currency: currency_code.toUpperCase(),
order_id: context.cart_id,
email: context.customer?.email,
callback_url: `${process.env.BACKEND_URL}/mypay/webhook`,
});
return {
id: payment.id,
data: {
payment_id: payment.id,
payment_url: payment.checkout_url,
status: payment.status,
},
};
} catch (e) {
return { error: e.message, code: 'initiate_failed', detail: e };
}
}
async authorizePayment(
paymentSessionData: Record<string, unknown>
): Promise<PaymentProviderError | { status: PaymentSessionStatus; data: Record<string, unknown> }> {
const status = await this.getPaymentStatus(paymentSessionData);
return { status, data: paymentSessionData };
}
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
const payment = await this.client.getPayment(paymentSessionData.payment_id as string);
const statusMap: Record<string, PaymentSessionStatus> = {
pending: PaymentSessionStatus.PENDING,
succeeded: PaymentSessionStatus.AUTHORIZED,
failed: PaymentSessionStatus.ERROR,
cancelled: PaymentSessionStatus.CANCELED,
};
return statusMap[payment.status] ?? PaymentSessionStatus.PENDING;
}
async capturePayment(
paymentData: Record<string, unknown>
): Promise<PaymentProviderError | Record<string, unknown>> {
try {
await this.client.capture(paymentData.payment_id as string);
return { ...paymentData, status: 'captured' };
} catch (e) {
return { error: e.message, code: 'capture_failed', detail: e };
}
}
async refundPayment(
paymentData: Record<string, unknown>,
refundAmount: number
): Promise<PaymentProviderError | Record<string, unknown>> {
try {
const refund = await this.client.refund(
paymentData.payment_id as string,
Math.round(refundAmount)
);
return { ...paymentData, refund_id: refund.id };
} catch (e) {
return { error: e.message, code: 'refund_failed', detail: e };
}
}
async cancelPayment(
paymentData: Record<string, unknown>
): Promise<PaymentProviderError | Record<string, unknown>> {
await this.client.cancel(paymentData.payment_id as string);
return { ...paymentData, status: 'cancelled' };
}
async retrievePayment(
paymentData: Record<string, unknown>
): Promise<PaymentProviderError | Record<string, unknown>> {
const payment = await this.client.getPayment(paymentData.payment_id as string);
return { ...paymentData, ...payment };
}
}
export default MyPayProvider;
Provider Registration
// medusa-config.ts
module.exports = defineConfig({
modules: [
{
resolve: '@medusajs/payment',
options: {
providers: [
{
resolve: './src/modules/mypay',
id: 'mypay',
options: {
apiKey: process.env.MYPAY_API_KEY,
secretKey: process.env.MYPAY_SECRET_KEY,
},
},
],
},
},
],
});
Webhook Handler
// src/api/mypay/webhook/route.ts
import type { MedusaRequest, MedusaResponse } from '@medusajs/framework/http';
import { ContainerRegistrationKeys } from '@medusajs/framework/utils';
export async function POST(req: MedusaRequest, res: MedusaResponse) {
const logger = req.scope.resolve(ContainerRegistrationKeys.LOGGER);
const signature = req.headers['x-signature'] as string;
const isValid = verifySignature(JSON.stringify(req.body), signature, process.env.MYPAY_SECRET_KEY!);
if (!isValid) {
return res.status(403).json({ message: 'Invalid signature' });
}
const { payment_id, status } = req.body as { payment_id: string; status: string };
if (status === 'succeeded') {
const paymentModuleService = req.scope.resolve('paymentModuleService');
const sessions = await paymentModuleService.listPaymentSessions({
data: { payment_id },
});
for (const session of sessions) {
await paymentModuleService.authorizePaymentSession(session.id, req.body);
}
}
res.status(200).json({ received: true });
}
Official Stripe Provider
For Stripe there's official @medusajs/payment-stripe:
npm install @medusajs/payment-stripe
// medusa-config.ts
{
resolve: '@medusajs/payment-stripe',
options: {
apiKey: process.env.STRIPE_API_KEY,
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
capture: true, // automatic capture
},
}
Official provider supports Stripe webhooks, 3DS, refunds and Stripe Connect out of the box.







