PayPal is worth adding for one reason: a meaningful share of customers will not complete a card checkout and will complete a PayPal one. In some categories it is a fifth of orders.
Mechanically it differs from a card processor in ways that matter — approval happens inside PayPal's own flow, and disputes are adjudicated by PayPal rather than a card network.
Configuration
PayPal does not ship as a first-party Medusa module, so you use a community provider or write one against PayPal's Orders API. Either way the registration looks the same:
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
resolve: "./src/modules/payment-paypal",
id: "paypal",
options: {
clientId: process.env.PAYPAL_CLIENT_ID,
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
sandbox: process.env.NODE_ENV !== "production",
webhookId: process.env.PAYPAL_WEBHOOK_ID,
},
},
],
},
}Then enable it per region in the admin, and note which currencies your PayPal account actually supports — the list is shorter than Stripe's, and an unsupported currency fails at order creation with a message about the merchant account rather than the currency.
The flow
PayPal inverts the usual sequence. Instead of collecting details on your page:
- Your backend creates a PayPal order and returns its id.
- The PayPal button opens a popup; the customer approves inside PayPal.
- The popup returns an approval token to your page.
- Your backend captures or authorises against the PayPal order.
- You complete the Medusa cart.
"use client"
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js"
export function PayPalCheckout({
cart,
sessionData,
onApproved,
}: {
cart: StoreCart
sessionData: { paypal_order_id: string }
onApproved: () => Promise<void>
}) {
return (
<PayPalScriptProvider
options={{
clientId: process.env.NEXT_PUBLIC_PAYPAL_CLIENT_ID!,
currency: cart.currency_code.toUpperCase(),
intent: "authorize",
}}
>
<PayPalButtons
style={{ layout: "vertical", label: "pay" }}
createOrder={async () => sessionData.paypal_order_id}
onApprove={async () => {
// The server authorises against PayPal, then completes the cart.
await onApproved()
}}
onError={(err) => console.error("[paypal]", err)}
/>
</PayPalScriptProvider>
)
}createOrder returns the id your payment session already created, rather than creating a second order. Creating one client-side is the classic mistake: PayPal ends up with an order your backend does not know about, and the amounts drift.
Authorise, then capture
Use intent: "authorize" and capture at fulfilment, matching the card default. Cancelling before shipping voids the authorisation, so no refund fee and no accounting cleanup. PayPal authorisations are honoured for a limited window — check current terms and capture within it. Authorise versus capture.
Webhooks
More important here than with cards, because approval happens entirely outside your request cycle. A customer can approve in the popup and close the tab before your completion call runs.
Endpoint: https://api.yourdomain.com/hooks/payment/paypal_paypal
Events:
CHECKOUT.ORDER.APPROVED
PAYMENT.AUTHORIZATION.CREATED
PAYMENT.CAPTURE.COMPLETED
PAYMENT.CAPTURE.DENIED
CUSTOMER.DISPUTE.CREATEDVerify signatures using PayPal's verification endpoint with your webhookId, and make handlers idempotent — PayPal redelivers.
Subscribe to CUSTOMER.DISPUTE.CREATED specifically. Disputes have response deadlines, and finding out by email a week later is how you lose them.
Disputes
PayPal adjudicates its own disputes and the process differs from card chargebacks:
| Aspect | PayPal | Card network |
|---|---|---|
| Adjudicator | PayPal | Issuing bank |
| Window | Typically 180 days | Varies, often 120 |
| Evidence | Tracking, delivery proof | Tracking, AVS, 3DS |
| Fee | Varies by case type | Usually charged |
The practical requirement is the same in both: upload tracking numbers to the transaction. Item-not-received disputes with delivery confirmation are usually resolved in the merchant's favour, and without it they are usually not. Wire this into your fulfilment flow as an automatic step rather than a manual one.
Testing
Use sandbox credentials with a sandbox buyer account. Cover:
- Approval and capture — the happy path.
- Cancel inside the popup. Your cart must survive.
- Close the popup mid-flow, then retry.
- Webhook redelivery, including duplicates.
- Refund, partial and full.
The second and third are where implementations break, because they are the paths nobody demos.
Practical guidance
Put the PayPal button above the card form. Customers who want it should not scroll past a card form to find it.
Do not require an account first. PayPal supports guest card payments through the same button in most markets.
Reconcile daily. Compare PayPal captures against Medusa payments. Asynchronous approval means occasional drift, and daily reconciliation turns a mystery into a line item.
Reconciliation and reporting
PayPal settles on its own schedule and its own fee structure, which means its numbers will not line up with your card processor's without deliberate work.
Three practices that keep the books honest:
Store the PayPal capture id on the Medusa payment. It is the only reliable join between your order and the settlement report.
Reconcile daily rather than monthly. Asynchronous approval means occasional drift — a payment captured at PayPal with no matching Medusa order. Daily, it is one line to investigate; monthly, it is a spreadsheet exercise.
Record fees per transaction, not as a monthly lump. PayPal's rate varies by method, currency and cross-border status, so a blended assumption will misstate margin per order.
Currency and cross-border
PayPal converts currencies itself, and its rate is not your bank's. Two consequences:
- Charge in the customer's currency where your account supports it, so the buyer sees a familiar amount and PayPal's conversion applies to your settlement rather than their charge.
- Cross-border transactions carry a higher fee. If you sell internationally in volume, model it — the difference between domestic and cross-border rates is material at scale.
Check which currencies your PayPal account actually supports before configuring a region around one. The list is shorter than Stripe's, and an unsupported currency fails at order creation with a message about the merchant account rather than the currency.
Testing checklist
Sandbox credentials and a sandbox buyer account, then work through every path — including the ones nobody demos:
[ ] Approve and capture — the happy path
[ ] Cancel inside the popup; cart survives intact
[ ] Close the popup mid-flow, then retry successfully
[ ] Approve, then close the tab before completion — webhook completes the order
[ ] Webhook redelivery of an already-processed event
[ ] Partial refund, then full refund
[ ] Currency your account does not support — fails clearlyThe fourth line is the one that finds real bugs. It is also the exact behaviour of a customer on a slow mobile connection, which makes it far more common in production than in testing.
We wire PayPal alongside cards on most consumer builds. Ask about it.
Frequently asked questions
Does Medusa support PayPal?
Not as a first-party module. You use a community provider or write one against PayPal's Orders API using the payment provider interface, then register and enable it per region like any other provider.
Should I offer PayPal instead of cards?
Alongside, not instead. PayPal is additive — it captures customers who abandon card forms — while removing cards would cost you more than it gains. Place the button above the card form.
How does the PayPal checkout flow differ from Stripe?
Approval happens inside PayPal's popup rather than on your page. Your backend creates a PayPal order, the customer approves it in PayPal, and your backend then authorises or captures against that order before completing the Medusa cart.
Do I need PayPal webhooks?
Yes. Approval and capture complete outside your request cycle, so a customer can approve and close the tab before your completion call runs. Webhooks are the only reliable way to reconcile, and dispute notifications arrive through them too.
How are PayPal disputes handled?
PayPal adjudicates them itself, on a longer window than most card networks. Tracking numbers uploaded to the transaction are the decisive evidence for item-not-received cases, so automate that as part of fulfilment.
Can I capture PayPal payments at fulfilment rather than checkout?
Yes — use `intent: "authorize"` and capture when you ship, matching the card default. Authorisations are valid for a limited window, so capture within it or re-authorise.
The Medusa Checkout Flow: Every Step, and Where It Breaks
Address, shipping, payment session, complete. The four stages of a Medusa checkout, the state machine underneath, and the failure modes that cost orders.
Medusa Multi-Region: Currencies, Tax and Selling Internationally
Regions carry currency, tax, payment methods and shipping. How to model international selling without accidentally creating twelve stores.
Razorpay with Medusa: Payments for the Indian Market
UPI, netbanking, RuPay and the mandate rules that make Indian payments their own discipline. Integrating Razorpay with Medusa properly.



