Razorpay Magento Integration — A Complete India + INR Setup Guide
Razorpay ships an official Magento module — razorpay/razorpay-magento on Marketplace and GitHub — that covers Cards, NetBanking, UPI Collect, UPI Intent, Wallets, EMI, and PayLater. The defaults work; production does not. This is the install command, the system.xml API-key config, the webhook signature check for payment.captured and refund.processed, the RBI 2-factor authentication redirect, the UPI AutoPay e-mandate flow for subscriptions, and the T+1 to T+3 settlement timing every Indian merchant needs to plan around. Magento 2.4.4 — 2.4.9, INR-only stores.
The Razorpay Magento integration is the official razorpay/razorpay-magento module that ships every India-focused payment method — Cards, NetBanking, UPI Collect, UPI Intent, Wallets, EMI, PayLater — as one Magento payment gateway, wired against Razorpay's REST API at api.razorpay.com/v1[1]. This guide is the production checklist: install command, system.xml API config, webhook signature verification, RBI 2-factor authentication redirect, UPI AutoPay subscriptions, and T+1 to T+3 settlement timing.
Razorpay's official module covers seven payment methods, but only three of them work out of the box for production.
Of the 11 Razorpay-on-Magento engagements shipped from kishansavaliya.com in 2025-2026, every one needed work beyond the README — webhook verification, RBI 2FA testing, UPI AutoPay, refund reconciliation. Production-ready takes a working day.
The Razorpay module's defaults are correct for sandbox. They are unsafe for production until the webhook, the 2FA callback timeout, and the settlement reconciliation cron are in place.
Six sections — install, config, webhook signature, RBI 2FA, subscriptions, settlement reconciliation. Magento 2.4.4 — 2.4.9, PHP 8.1+, INR-only stores.
1. Install the official Razorpay Magento module
Razorpay's razorpay/razorpay-magento package is on Packagist and mirrored on the Magento Marketplace[2]. The Composer route is the only one to use in 2026 — Marketplace ZIP installs are deprecated and skip the lock-file pinning that keeps the SDK version stable across deploys.
Composer install
composer require razorpay/razorpay-magento
bin/magento module:enable Razorpay_Magento
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy -f
bin/magento cache:flushThe package pulls razorpay/razorpay (PHP SDK) and razorpay/curl as transitive dependencies. Force the latest with composer require razorpay/razorpay-magento:^4.6.
What ships in the module
| Component | Path |
|---|---|
Payment method (MethodInterface) | Model/PaymentMethod.php |
| Admin config (keys, status, capture mode) | etc/adminhtml/system.xml |
Webhook controller — /razorpay/webhook | Controller/Webhook/Index.php |
| Order-place observer (creates Razorpay order) | Observer/SalesOrderPlaceAfter.php |
| Knockout payment block (Luma) | view/frontend/web/js/view/payment/method-renderer |
The Hyvä Checkout adapter is a separate package — hyva-themes/magento2-razorpay[3]. Install it after the official module on Hyvä storefronts.
2. Configure API keys and capture mode
Razorpay issues two key pairs per account — Test mode (rzp_test_) and Live mode (rzp_live_). Both live in the dashboard under Settings → API Keys. Never commit the Live secret.
Admin path
Stores → Configuration → Sales → Payment Methods → Razorpay. The fields that matter:
| Field | Production rule |
|---|---|
| Title | List the methods the customer expects — UPI first for India |
| Key ID + Key Secret | Inject via env.php, never the admin UI |
| Webhook Secret | 32+ char random string, pasted to the Razorpay dashboard verbatim |
| Payment Action | Authorize and Capture — auto-capture matches T+1 settlement |
| New Order Status | Processing — Pending only fits manual-capture B2B workflows |
env.php injection (recommended)
<?php
// app/etc/env.php
'system' => [
'default' => [
'payment' => [
'razorpay' => [
'key_id' => getenv('RAZORPAY_KEY_ID'),
'key_secret' => getenv('RAZORPAY_KEY_SECRET'),
'webhook_secret' => getenv('RAZORPAY_WEBHOOK_SECRET'),
],
],
],
],Magento reads from env.php first when the same path exists in core_config_data. The deploy script reads from the CI vault and exports the env vars before bin/magento runs.
Sandbox smoke test
curl -X POST https://api.razorpay.com/v1/orders \
-u rzp_test_xxxxxxxxxxxxxx:test_secret_xxxxxxxxxxxxxx \
-H 'Content-Type: application/json' \
-d '{
"amount": 50000,
"currency": "INR",
"receipt": "order_smoke_001"
}'A 200 response with an id field starting order_ means the key pair is valid. Amount is in paise — 50000 paise is ₹500. Currency must be INR for Indian-registered Razorpay accounts.
3. Webhook signature verification — the part that is not optional
Razorpay fires three webhooks every production storefront must handle — payment.captured, payment.failed, refund.processed. Each arrives with an X-Razorpay-Signature header computed as HMAC-SHA256(webhook_secret, raw_body)[4]. The official verifier is correct — the trap is custom observers stacked on top that capture the order before the verifier runs, marking failed payments as paid.
Verifier signature pattern
<?php
// app/code/Panth/RazorpayHardening/Controller/Webhook/Verify.php
namespace Panth\RazorpayHardening\Controller\Webhook;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\Request\Http as HttpRequest;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Store\Model\ScopeInterface;
use Psr\Log\LoggerInterface;
class Verify implements HttpPostActionInterface
{
public function __construct(
private HttpRequest $request,
private ScopeConfigInterface $config,
private JsonFactory $jsonFactory,
private LoggerInterface $logger
) {}
public function execute()
{
$rawBody = $this->request->getContent();
$signature = $this->request->getHeader('X-Razorpay-Signature');
$secret = $this->config->getValue(
'payment/razorpay/webhook_secret',
ScopeInterface::SCOPE_STORE
);
$expected = hash_hmac('sha256', $rawBody, (string)$secret);
// Constant-time comparison — never use ===
if (!hash_equals($expected, (string)$signature)) {
$this->logger->warning('Razorpay webhook signature mismatch');
$result = $this->jsonFactory->create();
$result->setHttpResponseCode(400);
return $result->setData(['status' => 'invalid_signature']);
}
$payload = json_decode($rawBody, true);
$event = $payload['event'] ?? '';
// Dispatch by event — payment.captured, payment.failed, refund.processed
return $this->jsonFactory->create()->setData(['status' => 'ok']);
}
}The three events to dispatch on
| Event | Magento action |
|---|---|
payment.captured | Mark processing, create invoice, send confirmation email |
payment.failed | Mark canceled, restock items, send failure notice |
refund.processed | Flip credit memo to refunded, not open |
Webhook URL registration and retries
In the Razorpay dashboard under Settings → Webhooks → Add New Webhook, set the URL to https://your-store.example/razorpay/webhook and tick only the three events above. order.paid and payment.authorized fire alongside payment.captured and cause double processing on auto-capture configs.
Razorpay retries failed deliveries on exponential backoff for 24 hours — 5s, 30s, 5m, 30m, 2h, 12h[4]. The controller must respond with HTTP 200 inside 5 seconds. Move slow work (email, inventory adjust) into a Magento queue consumer.
4. RBI's 2-factor authentication mandate and the 3-step redirect
The Reserve Bank of India has mandated 2-factor authentication on all domestic card-not-present transactions since 2021[5]. Razorpay handles the OTP for cards, bank login for NetBanking, and UPI PIN prompt for UPI — the storefront keeps the order pending across redirects and only finalizes on the webhook.
The 3-step flow
- Customer clicks Place Order. Magento POSTs to
/v1/ordersand gets arazorpay_order_id, then opens the Razorpay Checkout iframe. - Customer picks UPI or Cards. Razorpay redirects to the bank's 2FA page (OTP for cards, UPI PIN for UPI).
- Bank confirms. Razorpay redirects back to Magento's callback route with
razorpay_payment_idandrazorpay_signature. Magento verifies the signature, then waits for thepayment.capturedwebhook to flip the order to processing.
Callback signature verification — different from webhook
The callback signature is HMAC-SHA256 of razorpay_order_id|razorpay_payment_id using the API key secret (not the webhook secret). Reusing the wrong secret is the most common silent failure.
<?php
// app/code/Panth/RazorpayHardening/Helper/CallbackVerifier.php
public function verify(string $orderId, string $paymentId, string $signature): bool
{
$secret = $this->config->getValue(
'payment/razorpay/key_secret',
ScopeInterface::SCOPE_STORE
);
$expected = hash_hmac('sha256', $orderId . '|' . $paymentId, (string)$secret);
return hash_equals($expected, $signature);
}What can go wrong in the 2FA window
- Customer closes the tab during OTP entry — Magento has a pending order with no payment. The reconciliation cron must call
/v1/payments?order_id=...after 15 minutes and cancel if no successful payment exists. - OTP times out at the bank — Razorpay fires
payment.failed. The webhook handler must move the order to canceled. - Redirect URL fails but bank confirmed — money is debited, customer lands on a Magento 404. The webhook still arrives — the order is completed from the webhook path, not the callback path. Keep both routes working.
The reconciliation cron
curl -G https://api.razorpay.com/v1/payments \
-u rzp_live_xxxxxxxxxxxxxx:live_secret_xxxxxxxxxxxxxx \
--data-urlencode "from=$(date -v-30M +%s)" \
--data-urlencode "to=$(date +%s)" \
--data-urlencode "count=100"The cron runs every 5 minutes, pulls the last 30 minutes of payments, and cross-checks against Magento's pending orders. Razorpay's Settlements → Reconciliation Report is the source of truth for daily numbers — never the Magento DB alone.
5. Subscriptions — UPI AutoPay and NetBanking recurring via the e-mandate flow
The default module handles single-payment orders. Recurring billing — subscription products, installments, donation auto-debits — needs the Razorpay Subscriptions API plus the e-mandate flow for UPI AutoPay and NetBanking recurring.
The Razorpay Subscriptions object model
| Object | Magento mapping |
|---|---|
plan | Created admin-side, one per recurring product |
customer | Created on first signup, ID stored on customer_entity |
subscription | One per active recurring product per customer; ID on quote_extension_attributes |
token | Customer's e-mandate consent, created during initial 2FA |
invoice | One Magento order per Razorpay invoice via subscription.charged |
UPI AutoPay e-mandate creation
curl -X POST https://api.razorpay.com/v1/subscriptions \
-u rzp_live_xxxxxxxxxxxxxx:live_secret_xxxxxxxxxxxxxx \
-H 'Content-Type: application/json' \
-d '{
"plan_id": "plan_MJ3kRzn4Yp8XQK",
"customer_notify": 1,
"total_count": 12,
"start_at": 1748822400,
"notes": {"magento_customer_id": "4271"}
}'The response includes a short_url field — the e-mandate auth page. Redirect the customer there on Magento Subscribe click. The UPI app prompts for an e-mandate approval (PIN + consent screen), and Razorpay fires subscription.activated on success.
The subscription webhooks to handle
{
"event": "subscription.activated",
"payload": {
"subscription": {
"entity": {
"id": "sub_MJ3kPQrTzS9XJk",
"plan_id": "plan_MJ3kRzn4Yp8XQK",
"customer_id": "cust_MJ2jOPqRyT0WIj",
"status": "active",
"charge_at": 1751414400
}
}
}
}Magento creates the panth_subscription row on this webhook, links it to customer_entity, and schedules the next billing reminder. The recurring charge fires through subscription.charged — each charge generates a fresh Magento order via the same observer that handles one-off payments.
The UPI AutoPay PIN entry race
The UPI e-mandate consent screen has a 5-minute timeout. If the customer takes longer, the mandate fails silently and the subscription stays in created state forever. Poll GET /v1/subscriptions/<id> every 10 minutes for an hour; if still created after 60 minutes, cancel the local row and email the customer.
6. Settlement timing and refund liquidity planning
Razorpay settles to the merchant bank account on a rolling T+1 to T+3 schedule based on KYC tier[6]. New accounts default to T+3; verified merchants drop to T+2; high-volume verified merchants negotiate T+1. The settlement timing affects refund liquidity and the Magento Pending dashboard.
| Payment method | Settlement to bank | Refund timing |
|---|---|---|
| UPI Intent / Collect | T+1 (verified) to T+3 (new) | Instant to T+0 |
| Credit Card | T+2 to T+3 | 5-7 working days |
| Debit Card | T+1 to T+2 | 5-7 working days |
| NetBanking | T+1 to T+2 | 3-5 working days |
| Wallets (Paytm, Mobikwik) | T+1 | Instant |
| EMI on Cards | T+2 to T+3 | 5-7 working days |
| PayLater (Simpl, LazyPay) | T+1 to T+2 | 3-5 working days |
Refund liquidity
If today's GMV is ₹1,000,000 and a ₹50,000 refund hits at T+0 — before any settlement has arrived — Razorpay funds the refund from the next settlement, debiting the merchant balance. New accounts can run negative if refunds exceed the unsettled balance.
Settlement reconciliation script
curl -G https://api.razorpay.com/v1/settlements/recon/combined \
-u rzp_live_xxxxxxxxxxxxxx:live_secret_xxxxxxxxxxxxxx \
--data-urlencode "year=2026" \
--data-urlencode "month=5" \
--data-urlencode "day=20" \
--data-urlencode "count=1000" > rzp-settlement-2026-05-20.jsonA nightly Magento cron pulls this JSON, joins to sales_order on razorpay_payment_id, and inserts a row into panth_settlement_reconciliation. The finance team queries that table instead of the live Razorpay dashboard.
The five things to verify before flipping the Live mode toggle
- Webhook URL is HTTPS, replies 200 within 5 seconds, and the verifier uses
hash_equals. - Live
key_secretis inenv.phpvia a vault env var — never incore_config_data, never in git. - Reconciliation cron registered in
crontab.xmlat*/5 * * * *and run once in staging. - A sandbox test order has cleared all 7 payment methods with exactly one Magento order each — no duplicates from
order.paidfiring alongsidepayment.captured. - Refund flow tested end-to-end — credit memo creation, refund API call,
refund.processedwebhook arrival, credit memo flipped to refunded.
Hyvä Checkout specifics
The Hyvä adapter (hyva-themes/magento2-razorpay) replaces the Luma Knockout payment renderer with a Magewire component. Same admin path, same env vars, same webhook controller — only the frontend block name changes, registered via hyva_checkout_components.xml. Verifiers are shared with the Luma path.
Indian compliance touches you do not need to build yourself
Razorpay handles GST invoice generation for the gateway fees, TDS deduction under Section 194-O for marketplace flows, and the merchant settlement statements the chartered accountant needs at year-end. None of these need Magento code — the merchant downloads them from Account → Documents → Tax Invoices. The order-level GST invoice for the customer is a separate Magento tax setup, unrelated to Razorpay.
FAQ
Is the official razorpay/razorpay-magento module free?
Yes — MIT-licensed on GitHub. Razorpay charges transaction-based fees (2% for domestic cards and NetBanking, 0% for UPI on the standard plan, negotiated rates for high-volume merchants). No Magento licensing fee.
Can I run Razorpay alongside other Magento payment methods like Stripe or PayPal?
Yes. Magento supports unlimited active payment methods. The India pattern is Razorpay for INR, Stripe for international cards, PayPal as fallback — each gateway with its own webhook URL and reconciliation table.
What happens if the customer's UPI app is offline during checkout?
UPI Intent times out at 3 minutes. Razorpay fires payment.failed, the order moves to canceled, and the customer sees a retry button with the cart preserved.
How do I test the webhook locally without exposing my Magento dev box?
Use a reverse tunnel (ngrok, Cloudflare Tunnel) or Razorpay's own webhook simulator under Dashboard → Settings → Webhooks → Test. The simulator POSTs a signed payload — the verifier path is identical to production.
What is the minimum amount Razorpay accepts?
₹1 (100 paise). The amount field in POST /v1/orders is denominated in paise as an integer — ₹1.50 is amount: 150.
Does Razorpay Magento work on Magento 2.4.4 through 2.4.9?
Yes. Module versions 4.x support PHP 7.4 through 8.4 and Magento 2.4.4 — 2.4.9. The Hyvä Checkout adapter requires Magento 2.4.6+ for the registry-based component layout.
References
- Razorpay Developer Documentation, API Reference — Orders, Payments, Refunds. Live host
api.razorpay.com/v1, basic auth with Key ID and Key Secret. - Razorpay official Magento module on Packagist —
razorpay/razorpay-magento— mirrored to the Magento Marketplace. Source on GitHub atrazorpay/razorpay-magento. - Hyvä Themes, Magewire payment-method adapter for Razorpay —
hyva-themes/magento2-razorpayComposer package; Hyvä Checkout 1.1+ on Magento 2.4.6+. - Razorpay Webhooks documentation, Signature verification and retry policy — HMAC-SHA256 with
X-Razorpay-Signatureheader; 24-hour exponential-backoff retry schedule. - Reserve Bank of India, Additional Factor of Authentication for card-not-present transactions — domestic card-not-present 2FA mandate, active since 2021 and continuously enforced through 2026.
- Razorpay Help Center, Settlement Cycles by KYC Tier — T+1, T+2, T+3 cycles tied to verification level and monthly GMV.
- Production engagement traces from kishansavaliya.com Razorpay-on-Magento engagements, October 2025 — May 2026. Eleven INR-only storefronts shipped on Magento 2.4.4 — 2.4.9.
I am Kishan Savaliya, an Adobe-Certified Magento + Hyvä developer. I install razorpay/razorpay-magento, wire the webhook signature verifier, build the RBI 2FA reconciliation cron, set up UPI AutoPay subscriptions, and hand over the settlement reconciliation table the finance team needs. Fixed quote from $499 audit · $2,499 sprint · ~28h @ $25/hr. See Magento development services or hire me.