Overview
Icon for Peach

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 clipboardauthorizePayment cross-checks the amount Peach reports on Copy to clipboard/status against 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/status by Copy to clipboardcheckoutId and 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 clipboard000.400.101 and 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/status call, so a shopper who closes the tab before the redirect still gets their order.

Install

npm 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.

import { 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, // secret
merchantId: 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 HMAC
referer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slash
notificationUrl: 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:

import type { PeachOptions } from "medusa-payment-peach-payments/providers/peach"

All provider options (annotated) plus environment variables

options: {
// "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, // secret
merchantId: 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, // secret
referer: process.env.PEACH_REFERER, // allowlisted storefront origin, no trailing slash
notificationUrl: 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):

https://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 clipboarddefaultCurrency yourself if you want a fallback.
  • No server-side cancel. Peach's V2 API cannot cancel a checkout; Copy to clipboardcancelPayment is a no-op and unpaid checkouts expire after 30 minutes.
  • Capture is a no-op for Copy to clipboardDB. 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.

You may also like

Browse all integrations

Build your own

Develop your own custom integration

Build your own integration with our API to speed up your processes. Make your integration available via npm for it to be shared in our Library with the broader Medusa community.

gift card interface

Ready to build your custom commerce setup?