Peach
Peach Payments Checkout V2 provider
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.
Why reach for this instead of a quick custom provider
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:
- Amount-integrity gate. Copy to clipboard
authorizePaymentcross-checks the amount Peach reports on Copy to clipboard/statusagainst the amount the session was created for, and fails closed on any mismatch or a missing amount. A result code alone never completes an order. - Webhook re-confirmation. The webhook handler never acts on the notification body's claimed outcome or amount. It re-fetches Copy to clipboard
/statusby Copy to clipboardcheckoutIdand treats that as the only source of truth, so the result is independent of anything in the (forgeable) notification. - Fail-closed result mapping. Copy to clipboard
000.400.101and Copy to clipboard000.400.102(3DS "not participating" and "not enrolled") map to Copy to clipboarderror, not success. They are intermediate, not terminal; real Checkout V2 successes land on a terminal Copy to clipboard000.100.1*code. - Declined refunds are not silent. Peach returns HTTP 200 for a declined refund; the provider checks the result code and throws rather than reporting success.
- Two independent completion paths (authorize-on-return and webhook), both converging on the same Copy to clipboard
/statuscall, so a shopper who closes the tab before the redirect still gets their order.
Install
1npm install medusa-payment-peach-payments
Requirements: Medusa v2 with Copy to clipboard@medusajs/framework and Copy to clipboard@medusajs/medusa Copy to clipboard^2.13.0 (peer deps, not bundled), Node.js 20+, and a Peach Payments merchant account with Checkout V2 access (plus a sandbox entity for testing).
Register
Payment providers in Medusa v2 go in the Payment module's Copy to clipboardproviders array, not the top-level Copy to clipboardplugins array. Always import the provider subpath. A bare package import is intentionally not exported.
1234567891011121314151617181920import { defineConfig } from "@medusajs/framework/utils"module.exports = defineConfig({modules: [{resolve: "@medusajs/medusa/payment",options: {providers: [{resolve: "medusa-payment-peach-payments/providers/peach",id: "peach",options: {mode: process.env.PEACH_MODE, // "sandbox" | "production"clientId: process.env.PEACH_CLIENT_ID,clientSecret: process.env.PEACH_CLIENT_SECRET, // secretmerchantId: process.env.PEACH_MERCHANT_ID,entityId: process.env.PEACH_ENTITY_ID, // semi-public (reaches the browser)secretToken: process.env.PEACH_SECRET_TOKEN, // secret: webhook HMAC + refund HMACreferer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slashnotificationUrl: process.env.PEACH_NOTIFICATION_URL,
The runtime provider id is Copy to clipboardpp_<identifier>_<id>. With Copy to clipboardid: "peach" that is Copy to clipboardpp_peach_peach, which is the Copy to clipboardprovider_id your storefront selects and the segment Medusa uses for the webhook URL. For the full, typed option set read the JSDoc on Copy to clipboardPeachOptions:
1import type { PeachOptions } from "medusa-payment-peach-payments/providers/peach"
All provider options (annotated) plus environment variables
1234567891011121314151617181920options: {// "sandbox" or "production". Default "sandbox". Drives which Peach hosts the provider talks to.mode: process.env.PEACH_MODE,// OAuth credentials from the Peach Dashboard (Checkout API access).clientId: process.env.PEACH_CLIENT_ID,clientSecret: process.env.PEACH_CLIENT_SECRET, // secretmerchantId: process.env.PEACH_MERCHANT_ID,// The Checkout entity id. Doubles as `authentication.entityId` and the SDK `key` the storefront// passes to Checkout.initiate(), so it reaches the browser (semi-public), but do not commit it.entityId: process.env.PEACH_ENTITY_ID,// HMAC key for webhook verification AND the V1 refund endpoint. Required for refunds even if you// never wire up webhooks.secretToken: process.env.PEACH_SECRET_TOKEN, // secretreferer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slashnotificationUrl: process.env.PEACH_NOTIFICATION_URL, // your webhook URL (see Webhooks)shopperResultUrl: process.env.PEACH_SHOPPER_RESULT_URL, // where the shopper returns after paying
Env var Option Notes Copy to clipboardPEACH_MODE Copy to clipboardmode Copy to clipboardsandbox or Copy to clipboardproduction, default Copy to clipboardsandbox Copy to clipboardPEACH_CLIENT_ID Copy to clipboardclientId Copy to clipboardPEACH_CLIENT_SECRET Copy to clipboardclientSecret secret Copy to clipboardPEACH_MERCHANT_ID Copy to clipboardmerchantId Copy to clipboardPEACH_ENTITY_ID Copy to clipboardentityId reaches the browser, not a secret Copy to clipboardPEACH_SECRET_TOKEN Copy to clipboardsecretToken secret; webhook HMAC plus refund HMAC Copy to clipboardPEACH_REFERER Copy to clipboardreferer storefront origin, no trailing slash Copy to clipboardPEACH_NOTIFICATION_URL Copy to clipboardnotificationUrl your webhook URL Copy to clipboardPEACH_SHOPPER_RESULT_URL Copy to clipboardshopperResultUrl return target after paying Copy to clipboardPEACH_CANCEL_URL Copy to clipboardcancelUrl hosted-redirect cancel target Copy to clipboardPEACH_PAYMENT_TYPE Copy to clipboardpaymentType Copy to clipboardDB or Copy to clipboardPA, default Copy to clipboardDB Copy to clipboardPEACH_MERCHANT_NAME Copy to clipboardmerchantName Copy to clipboardPEACH_DEFAULT_CURRENCY Copy to clipboarddefaultCurrency fallback only, no ZAR default Copy to clipboardPEACH_DEFAULT_COUNTRY_CODE Copy to clipboarddefaultCountryCode fallback only
Webhooks
No custom route needed. Medusa auto-mounts Copy to clipboard{MEDUSA_BACKEND_URL}/hooks/payment/pp_peach_<id> for every registered provider. With Copy to clipboardid: "peach", register this URL in the Peach Dashboard as your checkout's webhook endpoint (and set it as Copy to clipboardnotificationUrl):
1https://your-backend.example.com/hooks/payment/pp_peach_peach
Peach surfaces a signing secret when you add the webhook; that becomes your Copy to clipboardsecretToken. 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: Copy to clipboarddocs/WEBHOOKS.md.
Refunds
Refunds use Peach's older V1 endpoint (Copy to clipboardPOST /v1/checkout/refund), signed with the same HMAC key as webhooks, so Copy to clipboardsecretToken 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 Copy to clipboardcheckoutId); the provider stores it on the session after authorization and throws a clear error if it is missing rather than guessing.
Testing in sandbox
Set Copy to clipboardmode: "sandbox" 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 Copy to clipboard4200000000000091 frictionless success Mastercard Copy to clipboard5200000000000007 frictionless success Amex Copy to clipboard374500262001008 frictionless success
Full list (challenge and decline scenarios): Peach's test and go-live reference.
Storefront integration
Backend-only. There is no npm storefront SDK because Peach's Checkout widget itself is a script-tag global, not a package. Copy to clipboardexamples/storefront/ has reference React and Next.js code: reading the session off the cart, mounting the embedded widget, and handling the return redirect.
Limitations
- Two-decimal currencies only. Amounts are formatted and compared at 2 decimals; zero-decimal (JPY) and three-decimal currencies are not supported, and magnitudes beyond roughly 9e13 minor units lose float precision.
- No built-in ZAR default. Set Copy to clipboard
defaultCurrencyyourself if you want a fallback. - No server-side cancel. Peach's V2 API cannot cancel a checkout; Copy to clipboard
cancelPaymentis a no-op and unpaid checkouts expire after 30 minutes. - Capture is a no-op for Copy to clipboard
DB. Immediate-capture settles at Peach when the shopper pays. Pre-auth (Copy to clipboardPA) capture and void use a separate Peach API this provider does not implement.
License
MIT. See Copy to clipboardLICENSE. 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.
