Zoho Payments
Accept payments with Zoho Payments
medusa-payment-zoho
Zoho Payments provider for Medusa v2 — India-first, with embedded checkout, hosted checkout, signed webhooks, and refunds.
Copy to clipboardmedusa-payment-zoho connects a Medusa v2 store to Zoho Payments in India (Copy to clipboardregion: "in", the default) or the United States (Copy to clipboardregion: "us"). It creates Zoho payment sessions, opens the Zoho Checkout Widget or Hosted Checkout, verifies payments server-side, accepts signed webhooks, and supports full and partial refunds.
This is an independent community integration. It is not affiliated with, endorsed by, or maintained by Zoho Corporation or Medusa.
Features
- Embedded Zoho Checkout Widget for card, UPI, and other methods enabled on your Zoho account.
- Zoho Hosted Checkout for strict-CSP, no-JS, or redirect-based storefronts.
- Signed webhooks over the exact raw body, with account and live/test-mode checks.
- Correct money handling — ISO-4217 fraction digits, decimal major-unit strings, Copy to clipboard
BigNumberarithmetic, no binary floating point. - Full and partial refunds against captured payments, with a local replay guard.
- Automatic capture, stated honestly — successful Zoho payments are already captured; Copy to clipboard
capturePaymentverifies instead of pretending to capture. - Safe authorization polling for widget results that arrive before Zoho's session status is final.
- Framework-agnostic browser helper with a typed, discriminated result and no Node.js or Medusa imports in the client bundle.
- Typed errors for configuration, authentication, API, validation, state, and webhook-verification failures.
- 100% test coverage enforced for lines, branches, functions, and statements.
Requirements
Requirement Details Medusa Copy to clipboard>=2.17.2 <3.0.0; developed and verified against 2.22.0 Node.js Copy to clipboard>=20 (this package); Node Copy to clipboard>=22.22.0 is the effective floor when running Medusa 2.22 Zoho Payments An active India or US merchant account Zoho OAuth An ORG client with the scopes below, created in the same data centre as the account Zoho widget (widget mode) A publishable API key from Zoho Settings → Developer Space Webhooks A publicly reachable HTTPS URL for your Medusa backend Database / Redis Whatever your Medusa deployment already uses; this package adds no storage or cache dependency
Required Zoho OAuth scopes:
1234ZohoPay.payments.CREATEZohoPay.payments.READZohoPay.refunds.CREATEZohoPay.refunds.READ
Webhook-management scopes are not required: Zoho webhooks are registered manually in the dashboard.
Installation
Install the package in your Medusa application:
1npm i medusa-payment-zoho
Release status: Copy to clipboardmedusa-payment-zoho is published on npm. Install the latest release with the command above. The repository also contains the full test suite, source, and release verification so you can audit the exact tarball you install.Using pnpm or yarn works too; the package ships ESM and CommonJS builds with TypeScript declarations.
Zoho setup
- Create or confirm your Zoho Payments account in the correct data centre: India (Copy to clipboard
payments.zoho.in) or US (Copy to clipboardpayments.zoho.com). The account, OAuth client, and configured region must all match. - Create an OAuth client in the Zoho API console:
- client type: ORG;
- redirect URI: for example Copy to clipboard
http://localhost:53682/callback(match the value used by the helper script); - scopes: the four scopes listed above.
- Generate a refresh token with the repository helper: Copy to clipboard
scripts/get-refresh-token.mjs. Configure the account ID, client ID, client secret, region, and redirect URI first, then run: The helper performs the one-time authorization-code exchange and prints a long-lived refresh token. It uses Node.js built-ins only and reads credentials from the environment. Treat the output as a secret: do not paste it into issues, logs, or commits.
1node scripts/get-refresh-token.mjs
- Collect the credentials from Zoho:
- Account ID — Payments account identifier, sent on every API call.
- API key — Developer Space browser key, required only for widget mode. This key is publishable by design; it is not the OAuth client secret or signing key.
- Signing key — Developer Space signing key used for webhooks and checkout signatures. Server-only.
- Store every secret in environment variables on the Medusa server. See Copy to clipboard
.env.examplefor the variable names.
Only a Zoho Account Owner or Admin can generate an org-level refresh token. A user can hold at most 20 refresh tokens; generating more removes the oldest.
Backend configuration
Register the provider under the Payment Module in Copy to clipboardmedusa-config.ts. The default region is India and the default checkout mode is the embedded widget:
1234567891011121314151617181920import { defineConfig, loadEnv } from "@medusajs/framework/utils"loadEnv(process.env.NODE_ENV || "development", process.cwd())module.exports = 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!,cookieSecret: process.env.COOKIE_SECRET!,},},modules: [{resolve: "@medusajs/medusa/payment",options: {providers: [
The provider identifier is Copy to clipboardzoho and the configured id is Copy to clipboardzoho, so Medusa stores the provider as Copy to clipboardpp_zoho_zoho. Keep that ID stable: it appears in payment sessions, the Admin, and the webhook URL. Use the Copy to clipboard/providers/zoho subpath exactly; the package root is not a module-provider default.
For hosted checkout, set Copy to clipboardcheckoutMode: "hosted" and provide both URLs:
1234checkoutMode: "hosted",successUrl: process.env.ZOHO_PAYMENTS_SUCCESS_URL!,failureUrl: process.env.ZOHO_PAYMENTS_FAILURE_URL!,// apiKey is not required in hosted mode
All options, validation rules, and defaults are documented in Configuration.
Webhooks
Register this URL in Zoho Payments under Settings → Developer Space → Webhooks:
1https://<your-medusa-backend>/hooks/payment/zoho_zoho
Enable at least:
- Copy to clipboard
payment.succeeded - Copy to clipboard
payment.pending - Copy to clipboard
payment.failed
The provider verifies Copy to clipboardX-Zoho-Webhook-Signature over the exact raw body and parses the event from those same verified bytes. It requires Copy to clipboardaccount_id, checks Copy to clipboardlive_mode when configured, and validates the payment amount/currency. A missing Copy to clipboardsigningKey fails closed.
Copy to clipboardpayment.failed is emitted per failed attempt. The provider reconciles it against the Zoho session when Zoho supplies a session ID, so a later successful attempt in the same session still completes the order. Medusa 2.22's stock webhook subscriber ignores Copy to clipboardFAILED, Copy to clipboardPENDING, and Copy to clipboardPENDING_AUTHORIZATION actions; a terminal failure remains available for reconciliation rather than silently becoming paid. Refund events are acknowledged but map to Copy to clipboardNOT_SUPPORTED; reconcile refunds with Copy to clipboardretrieveRefund or your own job.
Medusa's payment webhook route returns HTTP 200 before the provider finishes handling the event. A rejected signature therefore does not trigger a Zoho retry: monitor Copy to clipboardZOHO_WEBHOOK_VERIFICATION_ERROR and reconcile. See Webhooks for the full setup, retry behaviour, and a signed Copy to clipboardcurl test.
Enable the provider in Medusa Admin
- Start the Medusa backend after adding the module.
- Open Settings → Regions and edit the region that uses the matching currency.
- Under Payment Providers, enable Zoho (Copy to clipboard
pp_zoho_zoho). - Save the region and refresh the storefront.
If Zoho does not appear, confirm the module resolves, the backend restarted without configuration errors, and the region is saved. See Troubleshooting.
Storefront quickstart: embedded widget
The checkout flow is:
- Create or refresh the Medusa payment session for Zoho.
- Re-read the cart and pass the session's public Copy to clipboard
datato Copy to clipboardpayWithZohoCheckout. - Let the customer pay in the Zoho widget.
- Call Copy to clipboard
cart.complete(). Medusa asks the provider to verify the payment with Zoho; client-reported success is never trusted.
1234567891011121314151617181920"use client"import { useState } from "react"import Medusa from "@medusajs/js-sdk"import { payWithZohoCheckout } from "medusa-payment-zoho/client"const sdk = new Medusa({baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,})export function ZohoCheckoutButton({ cartId }: { cartId: string }) {const [message, setMessage] = useState<string>()async function handlePay() {setMessage("Preparing Zoho checkout…")const { cart } = await sdk.store.cart.retrieve(cartId)await sdk.store.payment.initiatePaymentSession(cart, {provider_id: "pp_zoho_zoho",
The widget result is a hint; Copy to clipboardcart.complete() is the authoritative step. If Zoho is still processing an asynchronous method, the order may be created with the payment pending until a signed webhook or status poll confirms capture.
The provider stores only publishable checkout data in the Medusa payment session: Copy to clipboardzohoSessionId, Copy to clipboardcheckoutMode, Copy to clipboardamount, Copy to clipboardcurrencyCode, Copy to clipboardcurrencySymbol, Copy to clipboarddescription, Copy to clipboardinvoiceNumber, Copy to clipboardexpiresAt, Copy to clipboardmedusaSessionId, and Copy to clipboardwidget (widget mode) or Copy to clipboardcheckoutUrl (hosted mode). OAuth secrets and the signing key are never included.
Full guide, session-expiry handling, CSP, and server-side widget signature verification: Storefront widget.
Storefront quickstart: hosted checkout
Set Copy to clipboardcheckoutMode: "hosted" and configure Copy to clipboardsuccessUrl and Copy to clipboardfailureUrl. Then redirect the browser:
1234567891011121314151617181920"use client"import Medusa from "@medusajs/js-sdk"import { redirectToZohoHostedCheckout } from "medusa-payment-zoho/client"const sdk = new Medusa({baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL!,publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY!,})export async function startHostedCheckout(cartId: string) {const { cart } = await sdk.store.cart.retrieve(cartId)await sdk.store.payment.initiatePaymentSession(cart, {provider_id: "pp_zoho_zoho",})const { cart: freshCart } = await sdk.store.cart.retrieve(cartId)const session = freshCart.payment_collection?.payment_sessions?.find((candidate) => candidate.provider_id === "pp_zoho_zoho")
On the return URL, verify Zoho's hosted-checkout signature on your server, then call Copy to clipboardsdk.store.cart.complete(cartId). A redirect is not proof of settlement: ACH can return Copy to clipboardin_progress, and the webhook is the backstop if the customer closes the browser. Success/failure route examples: Storefront hosted checkout.
Configuration options
Option Default Required Description Copy to clipboardaccountId — Yes Zoho Payments account ID. Copy to clipboardclientId — Yes OAuth ORG client ID. Copy to clipboardclientSecret — Yes OAuth client secret. Server-only. Copy to clipboardrefreshToken — Yes Long-lived offline refresh token. Server-only. Copy to clipboardregion Copy to clipboard"in" No Copy to clipboard"in" (India) or Copy to clipboard"us". Account and OAuth client must match. Copy to clipboardenvironment Copy to clipboard"live" No Copy to clipboard"live" or Copy to clipboard"sandbox" (India only). Copy to clipboardcheckoutMode Copy to clipboard"widget" No Copy to clipboard"widget" or Copy to clipboard"hosted". Copy to clipboardapiKey — Widget mode Publishable Zoho widget API key. Safe to expose to the browser. Copy to clipboardsigningKey — Strongly recommended Developer Space signing key for webhooks, widget, and hosted-return signatures. Copy to clipboardsuccessUrl — Hosted mode Hosted checkout success URL. HTTPS in production. Copy to clipboardfailureUrl — Hosted mode Hosted checkout failure URL. HTTPS in production. Copy to clipboardliveMode Copy to clipboardundefined No When set, reject webhooks whose Copy to clipboardlive_mode differs. Copy to clipboardpaymentDescription Copy to clipboard"Medusa order payment" No Customer-facing description, max 500 characters. Copy to clipboardbusinessName Copy to clipboardundefined No Displayed in the embedded widget header when set. Copy to clipboardallowedPaymentMethods Copy to clipboardundefined No Pass-through allow-list, for example Copy to clipboard["card", "upi"]. Copy to clipboardcurrencySymbols Copy to clipboard{} No Display-symbol overrides keyed by uppercase ISO code, e.g. Copy to clipboard{ INR: "₹" }. Copy to clipboardmetadata Copy to clipboard{} No Up to 3 extra non-sensitive Zoho metadata entries. Copy to clipboardtimeoutMs Copy to clipboard15000 No Per-request HTTP timeout, 1–120000 ms. Copy to clipboardmaxRetries Copy to clipboard3 No Retries for safe calls and OAuth, 0–10. Copy to clipboardretryBaseDelayMs Copy to clipboard500 No Exponential-backoff base, 0–60000 ms. Copy to clipboardpollAttempts Copy to clipboard3 No Authorization status polling attempts, 0–10. Copy to clipboardpollIntervalMs Copy to clipboard500 No Delay between authorization polls, 0–10000 ms. Copy to clipboardwebhookToleranceSeconds Copy to clipboard0 No Timestamp tolerance; Copy to clipboard0 disables the check. A positive value must cover Zoho's retry window. Copy to clipboardapiBaseUrl Region default No Advanced override of the Zoho Payments API base URL. Copy to clipboardaccountsBaseUrl Region default No Advanced override of the OAuth accounts base URL. Copy to clipboardhostedCheckoutBaseUrl Region default No Advanced override of the hosted-checkout base URL. Copy to clipboardfetch Global Copy to clipboardfetch No Custom fetch implementation for proxies or instrumentation. Copy to clipboardlogger Medusa logger No Structured logger with Copy to clipboarddebug, Copy to clipboardwarn, and Copy to clipboarderror.
Environment variable names used in the examples:
123456789101112ZOHO_PAYMENTS_ACCOUNT_ID=ZOHO_PAYMENTS_CLIENT_ID=ZOHO_PAYMENTS_CLIENT_SECRET=ZOHO_PAYMENTS_REFRESH_TOKEN=ZOHO_PAYMENTS_SIGNING_KEY=ZOHO_PAYMENTS_API_KEY=ZOHO_PAYMENTS_REGION=inZOHO_PAYMENTS_CHECKOUT_MODE=widgetZOHO_PAYMENTS_SUCCESS_URL=ZOHO_PAYMENTS_FAILURE_URL=ZOHO_PAYMENTS_BUSINESS_NAME=ZOHO_PAYMENTS_LIVE_MODE=false
These variables are read by your Copy to clipboardmedusa-config.ts, not automatically by the provider. See Configuration for the full reference.
What is supported
Capability Supported Notes Embedded Zoho Checkout Widget Yes Default mode; requires Copy to clipboardapiKey. Zoho Hosted Checkout Yes Opt-in; requires Copy to clipboardsuccessUrl and Copy to clipboardfailureUrl. Payment session create/update Yes Reuses a session only when amount, currency, mode, and expiry match. Authorization and capture Yes Zoho auto-captures; the provider verifies the captured payment. Full and partial refunds Yes Against a captured payment; asynchronous Zoho status. Signed webhooks Yes Copy to clipboardpayment.succeeded, Copy to clipboardpayment.pending, Copy to clipboardpayment.failed. India and US data centres Yes One provider instance per Zoho account. Zoho Sandbox Yes India-only; separate sandbox credentials and Copy to clipboardenvironment: "sandbox". Multi-currency Yes Subject to your Zoho account's supported currencies. Saved payment methods No Account-holder and saved-method APIs are not implemented in v1. Manual or partial capture No Zoho does not offer it. Cancel a pending payment No No Zoho API; the session expires (15 minutes by default). Cancel a captured payment No Refund instead. Refund webhook → Medusa No Acknowledged as Copy to clipboardNOT_SUPPORTED; reconcile with Copy to clipboardretrieveRefund. Payment links, payouts No Outside this provider's scope. Automatic webhook creation No Register webhooks manually in Zoho. Automatic reconciliation No Use the exported Copy to clipboardZohoPaymentsClient in your own job. Multi-account routing No One provider instance maps to one Zoho account. Built-in storefront UI No Use the documented helpers or hosted checkout.
A successful Zoho payment is already captured. There is no separate capture step, no partial capture, and no cancel API. Copy to clipboardcapturePayment re-reads the payment and confirms the capture; it never creates a second charge. Refund settlement can be asynchronous; the provider records Zoho's refund status and logs pending/initiated refunds for reconciliation.
Security
- Secrets stay server-side. Copy to clipboard
clientSecret, Copy to clipboardrefreshToken, Copy to clipboardsigningKey, and access tokens are never placed in payment-session Copy to clipboarddata, logs, errors, or browser bundles. The widget Copy to clipboardapiKeyis publishable by design. - Webhooks fail closed. The signature is verified over the exact raw body before any field is trusted. There is no development mode that skips verification.
- Account and mode are checked. A valid signature from another Zoho account or from test mode does not complete a live payment.
- Payment ownership is verified. Storefront-supplied session data cannot inject a prior payment or Zoho session ID; authorization checks the Zoho payment's reference and metadata against the current Medusa payment session.
- Amount and currency are reconciled. Zoho amounts are compared with the Medusa session before an order is treated as captured, and positive amounts that round below the currency's precision are rejected.
- The storefront never decides success. It may display a hint, but Medusa asks the provider to retrieve the Zoho session/payment before creating a payment.
- Hosted checkout URLs are capabilities. They contain Zoho's Copy to clipboard
access_key; do not log them, send them to analytics, or expose them in referrer URLs. - No card data touches your server. Using Zoho's widget or hosted page keeps card entry on Zoho's infrastructure. This reduces, but does not eliminate, PCI DSS scope; confirm your obligations with your acquirer and a qualified assessor.
Report vulnerabilities privately via the process in SECURITY.md.
Testing
123456pnpm install --frozen-lockfilepnpm lintpnpm typecheckpnpm test:coveragepnpm buildpnpm check
Copy to clipboardpnpm test:coverage enforces 100% lines, branches, functions, and statements. The suite covers amount formatting, status mapping, all three signature schemes, OAuth caching and retries, HTTP retry policy, both checkout modes, webhook authorization, failed-attempt reconciliation, refunds, and browser-helper behaviour.
Documentation
- Configuration
- Storefront: embedded widget
- Storefront: hosted checkout
- Webhooks
- Signature verification
- Troubleshooting
- Architecture
- Compatibility and limitations
- Contributing
- Security policy
License
MIT © Jayashakthi Vishnu.
Zoho, Zoho Payments, and related marks are trademarks of Zoho Corporation. Medusa is a trademark of Medusa, Inc. This project is an independent community integration and is not affiliated with either company.
