Unofficial Peach Payments payment provider for Medusa v2
An unofficial Peach Payments (Checkout V2) payment provider for Medusa v2. It generalizes a production-grade Medusa v2 Peach integration, carrying over the parts worth keeping: checkout creation, authorize-on-return completion with an amount-integrity check, webhook handling that re-confirms the outcome server-to-server before trusting it, and V1 HMAC refunds.
Not affiliated with, endorsed by, or supported by Peach Payments. A community plugin maintained independently. For Peach API questions that are not about this plugin's code, see developer.peachpayments.com.
A payment provider is easy to write and easy to get subtly, expensively wrong. This one carries the guards you would want from any integration, made explicit:
Requirements: Medusa v2 with and (peer deps, not bundled), Node.js 20+, and a Peach Payments merchant account with Checkout V2 access (plus a sandbox entity for testing).
Payment providers in Medusa v2 go in the Payment module's array, not the top-level array. Always import the provider subpath. A bare package import is intentionally not exported.
The runtime provider id is . With that is , which is the your storefront selects and the segment Medusa uses for the webhook URL. For the full, typed option set read the JSDoc on :
| Env var | Option | Notes |
|---|---|---|
| or , default | ||
| secret | ||
| reaches the browser, not a secret | ||
| secret; webhook HMAC plus refund HMAC | ||
| storefront origin, no trailing slash | ||
| your webhook URL | ||
| return target after paying | ||
| hosted-redirect cancel target | ||
| or , default | ||
| fallback only, no ZAR default | ||
| fallback only |
No custom route needed. Medusa auto-mounts for every registered provider. With , register this URL in the Peach Dashboard as your checkout's webhook endpoint (and set it as ):
https://your-backend.example.com/hooks/payment/pp_peach_peachPeach surfaces a signing secret when you add the webhook; that becomes your . You do not need a body parser: the provider verifies the signature from the raw body when it is preserved, and reconstructs the signed message from Medusa's already-parsed body when it is not. Verification fails closed: an unverifiable webhook is ignored, never trusted. Scheme details: .
Refunds use Peach's older V1 endpoint (), signed with the same HMAC key as webhooks, so is required even if you never receive a webhook. The refund body is flat, form-encoded key-value pairs (not nested JSON) with a V1 HMAC over the sorted keys. It is a genuinely different signing scheme from the checkout API, and an easy thing to trip over building from scratch. Refunds need the original transaction id (not the ); the provider stores it on the session after authorization and throws a clear error if it is missing rather than guessing.
Set and use your sandbox entity. Peach's sandbox skips the success screen and OTP/challenge prompts for known test cards (any future expiry; CVV 3 digits for Visa/Mastercard, 4 for Amex):
| Scheme | Card number | Outcome |
|---|---|---|
| Visa | frictionless success | |
| Mastercard | frictionless success | |
| Amex | frictionless success |
Full list (challenge and decline scenarios): Peach's test and go-live reference.
Backend-only. There is no npm storefront SDK because Peach's Checkout widget itself is a script-tag global, not a package. has reference React and Next.js code: reading the session off the cart, mounting the embedded widget, and handling the return redirect.
MIT. See . An independent, community-maintained project with no affiliation to Peach Payments. Use at your own risk and test thoroughly against your own Peach account before going live.
npm install medusa-payment-peach-payments1import { defineConfig } from "@medusajs/framework/utils"2
3module.exports = defineConfig({4 modules: [5 {6 resolve: "@medusajs/medusa/payment",7 options: {8 providers: [9 {10 resolve: "medusa-payment-peach-payments/providers/peach",11 id: "peach",12 options: {13 mode: process.env.PEACH_MODE, // "sandbox" | "production"14 clientId: process.env.PEACH_CLIENT_ID,15 clientSecret: process.env.PEACH_CLIENT_SECRET, // secret16 merchantId: process.env.PEACH_MERCHANT_ID,17 entityId: process.env.PEACH_ENTITY_ID, // semi-public (reaches the browser)18 secretToken: process.env.PEACH_SECRET_TOKEN, // secret: webhook HMAC + refund HMAC19 referer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slash20 notificationUrl: process.env.PEACH_NOTIFICATION_URL,21 shopperResultUrl: process.env.PEACH_SHOPPER_RESULT_URL,22 defaultCurrency: process.env.PEACH_DEFAULT_CURRENCY, // no ZAR baked in, set it yourself23 },24 },25 ],26 },27 },28 ],29})import type { PeachOptions } from "medusa-payment-peach-payments/providers/peach"1options: {2 // "sandbox" or "production". Default "sandbox". Drives which Peach hosts the provider talks to.3 mode: process.env.PEACH_MODE,4
5 // OAuth credentials from the Peach Dashboard (Checkout API access).6 clientId: process.env.PEACH_CLIENT_ID,7 clientSecret: process.env.PEACH_CLIENT_SECRET, // secret8 merchantId: process.env.PEACH_MERCHANT_ID,9
10 // The Checkout entity id. Doubles as `authentication.entityId` and the SDK `key` the storefront11 // passes to Checkout.initiate(), so it reaches the browser (semi-public), but do not commit it.12 entityId: process.env.PEACH_ENTITY_ID,13
14 // HMAC key for webhook verification AND the V1 refund endpoint. Required for refunds even if you15 // never wire up webhooks.16 secretToken: process.env.PEACH_SECRET_TOKEN, // secret17
18 referer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slash19 notificationUrl: process.env.PEACH_NOTIFICATION_URL, // your webhook URL (see Webhooks)20 shopperResultUrl: process.env.PEACH_SHOPPER_RESULT_URL, // where the shopper returns after paying21 cancelUrl: process.env.PEACH_CANCEL_URL, // hosted-redirect cancel target (not used by embedded)22 paymentType: process.env.PEACH_PAYMENT_TYPE, // "DB" capture-now (default) or "PA" pre-auth23 merchantName: process.env.PEACH_MERCHANT_NAME, // shown in the Peach checkout UI24
25 // Fallback currency, used only when a session or refund carries none. No ZAR default: with neither26 // a session currency nor a default set, the provider throws rather than guessing.27 defaultCurrency: process.env.PEACH_DEFAULT_CURRENCY,28 defaultCountryCode: process.env.PEACH_DEFAULT_COUNTRY_CODE, // ISO alpha-2 fallback, omitted if unset29
30 // Optional per-result-code overrides, checked before the built-in map (code only, no env var):31 // resultCodeOverrides: { "000.400.101": "authorized" }32 // Overriding a decline or error code logs a warning once, since it loosens a fail-closed default.33}