PayPal for Medusa
Accept PayPal and credit card payments in your Medusa v2 store — built by an official PayPal Partner.
✨ What you get
| Feature | Details |
|---|---|
| 🔵 PayPal Buttons | One-click PayPal checkout for your customers |
| 💳 Card Payments | Secure credit & debit card fields, hosted by PayPal (PCI compliant) |
| 🛠 Admin Dashboard | Connect your PayPal account and manage everything from Medusa Admin |
| 🌍 Test & Live modes | Try everything safely in Sandbox before going live |
| 🔐 3D Secure | Extra card security, configurable in one click |
| 🔄 Reliable by design | Payments are verified with PayPal at every step — webhooks, retries, and safety nets are handled for you |
✅ Requirements
- Medusa v2.12+
- Node.js 20+
- PostgreSQL
🚀 Installation
In your Medusa backend folder, run:
npm install @easypayment/medusa-payment-paypal
⚙️ Setup — 5 steps
1. Add the plugin to medusa-config.ts
import { loadEnv, defineConfig } from "@medusajs/framework/utils"
loadEnv(process.env.NODE_ENV || "development", process.cwd())
export default defineConfig({
projectConfig: {
databaseUrl: process.env.DATABASE_URL,
http: {
storeCors: process.env.STORE_CORS!,
adminCors: process.env.ADMIN_CORS!,
authCors: process.env.AUTH_CORS!,
jwtSecret: process.env.JWT_SECRET || "supersecret",
cookieSecret: process.env.COOKIE_SECRET || "supersecret",
},
},
plugins: [
{
resolve: "@easypayment/medusa-payment-paypal",
options: {},
},
],
modules: [
{
resolve: "@medusajs/medusa/payment",
options: {
providers: [
{
// PayPal Buttons
resolve: "@easypayment/medusa-payment-paypal/providers/paypal",
id: "paypal",
options: {},
dependencies: ["paypal_onboarding"],
},
{
// Card payments
resolve: "@easypayment/medusa-payment-paypal/providers/paypal_card",
id: "paypal_card",
options: {},
dependencies: ["paypal_onboarding"],
},
],
},
},
],
})
2. Run database migrations
npx medusa db:migrate
3. Connect your PayPal account
- Start your Medusa server
- Open Medusa Admin → Settings → PayPal
- Choose Sandbox (testing) or Live (real payments)
- Click Connect to PayPal and follow the steps
That's it — your credentials are saved automatically. Webhooks are registered for you too.
💡 Prefer manual setup? Click Insert credentials manually and paste your Client ID and Secret from developer.paypal.com.
4. Turn PayPal on in your region
Go to Medusa Admin → Settings → Regions → your region and enable:
| Payment provider | What it is |
|---|---|
pp_paypal_paypal | PayPal Buttons |
pp_paypal_card_paypal_card | Card payments |
5. Add PayPal to your storefront
The checkout UI comes as a separate package — install it in your storefront project:
📦 @easypayment/medusa-paypal-ui — ready-made PayPal components for Next.js storefronts, with a step-by-step guide.
🎨 Customize (optional)
Everything is managed in Medusa Admin → Settings → PayPal — changes apply instantly, no restart needed:
- PayPal Settings — turn PayPal on/off, button color, shape, and label
- Advanced Card Payments — turn card payments on/off, 3D Secure mode
- Additional Settings — capture vs. authorize, brand name shown at PayPal, invoice prefix
🔐 Going live — two things we recommend
PAYPAL_ENCRYPTION_KEY— set this to any long random text in your server's environment. Your PayPal credentials are then stored encrypted in the database. Keep the key safe: if it's lost, just reconnect PayPal.MEDUSA_BACKEND_URL— set this to your backend's public address (e.g.https://api.mystore.com) so PayPal can reach your store for payment notifications.
Advanced options (for developers — everything works without these)
| Variable | Default | What it does |
|---|---|---|
STOREFRONT_URL | (request origin, if listed in STORE_CORS) | Storefront address used for PayPal return/cancel pages (can also be set in Admin) |
PAYPAL_ENCRYPTION_STRICT | false | true = refuse to save credentials unencrypted |
PAYPAL_SELLER_NONCE | (partner service nonce) | Only change this if you also run your own PAYPAL_PARTNER_SERVICE_URL; it must match the nonce that service puts in its onboarding links |
PAYPAL_ADMIN_ORIGIN | first ADMIN_CORS entry | Restricts the onboarding popup to your admin URL |
PAYPAL_WEBHOOK_COMPLETE_CART | true | Safety net that finishes an order when the payment succeeded but the buyer's browser closed. false disables it |
PAYPAL_RATE_LIMIT_MAX / PAYPAL_RATE_LIMIT_WINDOW_MS | (off) / 60000 | Optional request limit for the public checkout routes |
PAYPAL_WEBHOOK_RATE_LIMIT_MAX / PAYPAL_WEBHOOK_RATE_LIMIT_WINDOW_MS | (off) / 60000 | Optional request limit for the webhook endpoint (separate from the above) |
PAYPAL_WEBHOOK_REPLAY_WINDOW_MINUTES | 60 | Rejects webhook deliveries older than this |
PAYPAL_WEBHOOK_STALE_PROCESSING_MS | 300000 | When the retry job re-picks-up interrupted webhook events |
PAYPAL_WEBHOOK_ID_LIVE / PAYPAL_WEBHOOK_ID_SANDBOX | (auto) | Manual webhook-id override (normally automatic) |
PAYPAL_HTTP_TIMEOUT_MS | 30000 | Timeout for calls to PayPal |
PAYPAL_CURRENCY | EUR | Fallback currency if none is configured |
PAYPAL_ALERT_WEBHOOK_URLS | (unset) | Comma-separated URLs that receive operational alerts (including dead-lettered webhook events) |
PAYPAL_SETTINGS_CACHE_MS | 15000 | In-process cache for plugin settings reads; 0 disables |
PAYPAL_SEND_SHIPPING_ADDRESS | false | true sends the cart's shipping address on PayPal orders (SET_PROVIDED_ADDRESS) — improves PayPal Seller Protection eligibility |
PAYPAL_PARTNER_SERVICE_URL | (built-in) | Override the partner onboarding service used by the connect flow |
PAYPAL_PARTNER_MERCHANT_ID_SANDBOX / PAYPAL_PARTNER_MERCHANT_ID_LIVE | (built-in) | Override the partner merchant ids used for onboarding |
Declined cards: when PayPal declines a capture, /store/paypal/capture-order responds 402 with a buyer-friendly message plus a structured decline object — { code, message, retryable, avs_code?, cvv_code? } — so storefronts can show "insufficient funds, try another card" and decide whether to offer a retry.
Monitoring: operational counters live in the paypal_metric table; every webhook is recorded in paypal_webhook_event with automatic retries (events that exhaust retries are dead-lettered and pushed to PAYPAL_ALERT_WEBHOOK_URLS); audit events are logged as JSON lines tagged "log":"paypal_audit" (secrets redacted).
💬 Support
- 🐛 Found a bug? Open an issue
- 💡 Questions or feature requests? Start a discussion
- 🛍 Storefront UI package: @easypayment/medusa-paypal-ui
📄 License
MIT © Easy Payment