Custom Checkout Integration Guide
Complete end-to-end guide to integrate Razorpay Custom Checkout. Build your own UI, create orders, submit payments, verify signatures and go live safely.
This is a complete, end-to-end guide to accept payments using Razorpay Custom Checkout. Custom Checkout gives you a JavaScript library (razorpay.js) so you can build your own payment UI and control the experience at a granular level. You render the form, collect payment details and submit them with createPayment, while Razorpay processes the payment securely.
Once you finish integrating Razorpay using this guide, you will be able to accept payments from your customers using Custom Checkout. To see how this compares with the other integration options, refer to the
.What you are responsible for:
- Building the checkout UI.
- Creating an order before payment.
- Submitting payment details with
createPayment. - Verifying the signature on your server.
- Capturing the payment so it settles.
Custom Checkout is more complex than Standard Checkout and needs a development team.
The end-to-end sequence for a Custom Checkout payment is as follows:
Customer Your Frontend Your Server Razorpay| | | || 1. Start checkout | | ||-------------------->| 2. Create order | || |---------------------->| 3. POST /v1/orders || | |--------------------->|| | | order_id || |<----------------------|<---------------------|| 4. Enter details | | ||-------------------->| 5. createPayment(data)| || |------------------------------------------------>|| | OTP / bank redirect / UPI approval ||<------------------------------------------------------------------->|| | 6. payment.success || | (payment_id, order_id, signature) || |---------------------->| 7. Verify signature || | | (HMAC-SHA256) || | | 8. Capture + fulfil || | |--------------------->|| | | 9. Webhook: captured || | |<---------------------|
- Create an order on your server (Step 1) and pass the
order_idto the browser. - Build your UI, fetch enabled methods and call
createPayment(Step 2). - Razorpay processes the payment (OTP, bank redirect or UPI approval).
- Verify the signature returned to
payment.successon your server (Step 3). - Capture the payment and confirm it with webhooks (Step 4).
Use Custom Checkout when you need full control over the look, feel and flow of the payment form, for example, to white-label the experience, display only selected payment methods or integrate external wallets. If you do not need that level of control,
is faster to build and uses the prebuilt Razorpay modal.Watch Out: Authorised is not the same as paid
A payment in the authorized state is not yet in your account. Capture it, or enable auto-capture, before the window expires. Uncaptured payments are auto-refunded. Enable auto-capture in Dashboard → Account & Settings → Payment Capture.
- A with KYC and business verification complete.
- from the Dashboard. Use Test Mode keys while building and replace them with Live Mode keys to go live.
- A server-side backend to call the Orders API and verify signatures.
- HTTPS on your website with a valid SSL/TLS certificate.
Watch Out: Never expose your Key Secret
The Key Secret is used only on your server. Never put it in frontend code or any public repository. Your Key ID is safe to use on the client.
An order should be created for every payment. It is a server-side call to the Orders API. The order_id in the response is passed to Checkout, which ties the order to the payment and protects the request from tampering.
Watch Out: Do not skip order creation
Payments made without an order_id cannot be captured and are automatically refunded. Create the order before initiating payment.
Create the order from your server using any of the Razorpay server SDKs or a direct API call.
curl -X POST https://api.razorpay.com/v1/orders \-u [YOUR_KEY_ID]:[YOUR_KEY_SECRET] \-H 'content-type:application/json' \-d '{"amount": 50000,"currency": "INR","receipt": "rcptid_11","notes": { "customer_id": "cust_abc123" }}'
Amount is in the smallest currency subunit (paise for INR). For example, ₹299 = 29900. receipt is optional with a maximum of 40 characters, and notes can hold up to 15 key-value pairs. Save the order_id (for example, order_DBJOWzybf0sJbb) linked to your internal record. You will need it in Step 2 and Step 3. Know more about the
Display only the methods activated on your account. Fetch them after instantiating Razorpay using the ready event.
var razorpay = new Razorpay({key: '<YOUR_KEY_ID>',image: 'https://i.imgur.com/n5tjHFD.jpg', // logo shown in the popup});razorpay.once('ready', function(response) {console.log(response.methods); // render only these methods in your UI})
The ready event depends on a network call to Razorpay. If it does not fire (slow network, ad blocker or a CSP blocking the CDN), do not leave the customer with an empty form. Set a timeout, and if the methods have not arrived, fall back to a minimal default set and let the customer retry.
var razorpay = new Razorpay({ key: '<YOUR_KEY_ID>' });var methodsRendered = false;var FALLBACK_METHODS = { card: true, upi: true, netbanking: true }; // safe minimum// 1. If the fetch has not returned in 5s, render the fallback set.var timer = setTimeout(function () {if (!methodsRendered) {methodsRendered = true;renderPaymentMethods(FALLBACK_METHODS);showRetryNotice(); // let the customer refresh to load all methods}}, 5000);// 2. When methods arrive, cancel the timer and render the real list.razorpay.once('ready', function (response) {clearTimeout(timer);if (!methodsRendered) {methodsRendered = true;renderPaymentMethods(response.methods);}});
Watch Out: Do not hardcode the full method list
The fallback is a minimal safety net, not a substitute for the fetched list. The methods enabled on your account change over time, so always prefer response.methods and treat the fallback as temporary. Never ship the complete method list as a static array, because it drifts from what is actually enabled.
Handy Tips: Browser and WebView compatibility
razorpay.jssupports the current and previous major versions of Chrome, Firefox, Safari and Edge. JavaScript and cookies must be enabled.- Inside a mobile app WebView, enable JavaScript and DOM storage, and allow third-party cookies so bank redirects and OTP pages load. For native apps, use the , , or SDK instead of a WebView.
- Do not use
redirect: trueinside a WebView unless you handle the return URL, as the customer is taken out of your app.
Include the script, preferably in the <head> of your page. Always load it from the Razorpay CDN rather than self-hosting, so updates and fixes apply automatically.
<script type="text/javascript" src="https://checkout.razorpay.com/v1/razorpay.js"></script>
var razorpay = new Razorpay({key: '<YOUR_KEY_ID>',image: 'https://i.imgur.com/n5tjHFD.jpg', // logo shown in the payment popupredirect: true});
Handy Tips: Customer Fee Bearer (CFB)
For card payments with CFB enabled, set redirect: true and include callback_url during instantiation.
After creating the order and collecting the customer's payment details, send them to Razorpay by calling createPayment. The data you submit depends on the payment method.
var data = {amount: 1000, // in currency subunitscurrency: "INR",email: 'gaurav.kumar@example.com',contact: '+919876543210',notes: {address: 'Ground Floor, SJR Cyber, Laskar Hosur Road, Bengaluru',},order_id: 'order_DBJOWzybf0sJbb', // Order ID from Step 1method: 'netbanking',bank: 'HDFC' // method-specific field};var btn = document.querySelector('#btn');btn.addEventListener('click', function(){// must be inside a user-initiated event so the popup is not blockedrazorpay.createPayment(data);razorpay.on('payment.success', function(resp) {// send these three fields to your server for verification (Step 3)alert(resp.razorpay_payment_id);alert(resp.razorpay_order_id);alert(resp.razorpay_signature);});razorpay.on('payment.error', function(resp){// Read the full error object, not just the description (see Error handling below)console.error({code: resp.error.code, // e.g. BAD_REQUEST_ERROR, GATEWAY_ERRORdescription: resp.error.description, // human-readable message to show the customersource: resp.error.source, // where it failed, e.g. customer, business, bankstep: resp.error.step, // e.g. payment_authenticationreason: resp.error.reason, // machine-readable cause, e.g. payment_failedorder_id: resp.error.metadata && resp.error.metadata.order_id,payment_id: resp.error.metadata && resp.error.metadata.payment_id});});})
Watch Out: Call createPayment inside a user action
createPayment must be called within an event listener triggered by a user action, such as a button click, or the browser blocks the popup.
The data object you pass to createPayment changes with the method. Common fields (amount, currency, email, contact, order_id) stay the same. Below are the method-specific fields.
razorpay.createPayment({amount: 5000,email: 'gaurav.kumar@example.com',contact: '9123456780',order_id: 'order_DBJOWzybf0sJbb',method: 'card','card[name]': 'Gaurav Kumar','card[number]': '4111111111111111','card[cvv]': '123','card[expiry_month]': '10','card[expiry_year]': '30'});
For the UPI Intent flow, QR flow, saved cards, bank transfer and other advanced flows, see the full
.Handy Tips: Handler function vs callback URL
With the handler function, the success response (razorpay_payment_id, razorpay_order_id and razorpay_signature) is returned to your payment.success handler. Collect these and send them to your server. With a callback URL, Razorpay POSTs the same fields to your URL. Know more about
Custom Checkout surfaces errors in two places. Both use the same error object structure, so handle them consistently.
- Client-side (
payment.errorevent): returned when a payment attempt fails, for example, a declined card or a cancelled OTP. - Server-side (Orders and other API responses): returned with a non-2xx HTTP status when an API call is invalid.
{"error": {"code": "BAD_REQUEST_ERROR","description": "Payment failed because of a temporary issue at the bank.","source": "bank","step": "payment_authentication","reason": "payment_failed","metadata": {"order_id": "order_DBJOWzybf0sJbb","payment_id": "pay_DBJP1H5nubc"}}}
Watch Out: Do not show only error.description
An alert(error.description) is not enough. Log code, source, step and reason so you can distinguish a customer-fixable decline (retry) from a genuine failure, and reconcile with the metadata.payment_id.
Yes. Custom Checkout is available beyond the web SDK. For native and cross-platform apps, use the dedicated SDKs rather than loading razorpay.js in a WebView:
Order creation and signature verification stay on your server and are identical across web and mobile.
The three fields returned to payment.success (or posted to your callback_url) come from the browser, so you must verify the signature on your server before fulfilling. The signature is an HMAC-SHA256 hash of order_id and payment_id, signed with your Key Secret.
const crypto = require('crypto');function verifyPaymentSignature(orderId, paymentId, receivedSig, keySecret) {const body = orderId + '|' + paymentId;const expectedSig = crypto.createHmac('sha256', keySecret).update(body).digest('hex');return crypto.timingSafeEqual(Buffer.from(expectedSig, 'hex'),Buffer.from(receivedSig, 'hex'));}// CRITICAL: retrieve order_id from YOUR database, not from the client.const order = await db.orders.findByInternalId(internalOrderId);const isValid = verifyPaymentSignature(order.razorpayOrderId,razorpay_payment_id,razorpay_signature,process.env.RAZORPAY_KEY_SECRET);if (isValid) {// Update order status to PAID and trigger fulfilment} else {// Signature mismatch -> reject, do NOT fulfil}
Each Razorpay server SDK ships a helper so you do not have to compute the HMAC yourself. Pass the order_id from your own database, the razorpay_payment_id and the razorpay_signature.
String secret = "YOUR_KEY_SECRET";JSONObject options = new JSONObject();options.put("razorpay_order_id", order.getRazorpayOrderId());options.put("razorpay_payment_id", razorpayPaymentId);options.put("razorpay_signature", razorpaySignature);boolean status = Utils.verifyPaymentSignature(options, secret);
Watch Out: Always verify the signature server-side
Use the order_id from your own database, not the razorpay_order_id from the browser, which could be spoofed. Use a timing-safe comparison (crypto.timingSafeEqual or hmac.compare_digest), not plain string equality. Store all three fields for audit and idempotency. A failed signature check means a tampered payment, so reject it.
Signature verification confirms authenticity, but an authorized payment is not yet in your account. Capture it and use webhooks so your server learns the outcome regardless of the browser.
- Enable auto-capture: configure in Dashboard → Account & Settings → Payment Capture, or call the . Capture settings only work with the Orders API integrated.
- Set up webhooks: Dashboard → Account & Settings → Webhooks. Use a publicly accessible HTTPS endpoint and a webhook secret of at least 32 characters. Subscribe to
payment.captured,payment.failed,refund.failedandpayment.dispute.createdat a minimum. - Validate every webhook: verify the
X-Razorpay-Signatureheader against the raw request body, return HTTP 200 within 5 seconds and process the event in a background worker.
const crypto = require('crypto');// Use the RAW request body, not the parsed JSON, or the signature will not match.app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {const received = req.headers['x-razorpay-signature'];const expected = crypto.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET).update(req.body) // raw Buffer.digest('hex');if (received !== expected) return res.status(400).send('invalid signature');res.status(200).send('ok'); // acknowledge fast// Then process asynchronously (idempotent on razorpay_payment_id).const event = JSON.parse(req.body);// switch (event.event) { case 'payment.captured': ... }});
For the full async webhook handler pattern, replay-attack guard, idempotency, Late Auth handling, monitoring metrics, rollback plan and logging strategy, see the shared deep-dives in the
. They apply identically to Custom Checkout.Test in Test Mode with test API keys, so no real money moves. The mock bank page provides Success and Failure buttons.
Use any random CVV and any future expiry date. For netbanking and wallets, choose any bank or wallet and use the Success or Failure buttons. See the full list of
and run through the .- KYC approved and Live Mode API Keys generated from the Dashboard.
- Test API keys replaced with Live keys in all environments, with
KEY_SECRETin environment variables. - Every payment creates a fresh Order, and the
order_id, amount and currency match the Order exactly. razorpay.jsloaded from the Razorpay CDN, not self-hosted.createPaymentcalled only from a user action.
- Signature verified server-side using HMAC-SHA256 with the
order_idfrom your DB. - Order fulfilled only after signature verification passes, using a timing-safe comparison.
razorpay_payment_idstored for idempotency, and the Key Secret never exposed to the frontend or logs.- Auto-capture configured, and goods not delivered before the payment is
captured.
- Webhook URL configured in the Dashboard (Live Mode), with the signature verified on every event.
payment.captured,payment.failed,refund.failedandpayment.dispute.createdsubscribed and handled.- Endpoint returns HTTP 200 within 5 seconds, with heavy processing done async.
- Duplicate events handled idempotently and stale events discarded.
Custom Checkout uses the razorpay.js library so you build and control your own payment UI and call createPayment with method-specific data. Standard Checkout loads checkout.js and shows the prebuilt Razorpay modal. Choose Custom Checkout for full UI control and white-labelling, and Standard Checkout for a faster build.
After creating an order and collecting the customer's details, call razorpay.createPayment(data) inside a user-initiated event. The data object includes the order_id, amount, currency, method and method-specific fields such as bank, card, wallet or vpa. Handle the result with the payment.success and payment.error event listeners.
Instantiate Razorpay and call razorpay.once('ready', ...) to fetch the available methods. Render only the methods returned in response.methods in your custom UI. Know more about the various
The fields returned to the browser can be forged by a malicious actor. Verifying the HMAC-SHA256 signature on your server with your Key Secret, using the order_id from your own database, proves the payment is authentic before you fulfil the order.
- Skipping order creation. Payments made without an
order_idcannot be captured and are auto-refunded. Always create the order server-side first. - Calling
createPaymentoutside a user action. Browsers block the popup. Call it inside a click or tap handler. - Verifying the signature with the client's
order_id. Use theorder_idfrom your own database, never the value returned in the browser. - Fulfilling on
authorized. Authorised is not paid. Fulfil only after the payment iscaptured(enable auto-capture). - Parsing the webhook body before verifying. Compute the signature on the raw request body, or it will never match.
- Relying only on the browser callback. The customer may close the tab. Treat the webhook as the source of truth.
- Hardcoding the payment methods. Fetch them with the
readyevent so the list matches what is enabled on your account. - Exposing the Key Secret. Keep it server-side and out of frontend code, logs and repositories.
Most often the payment is authorized but not yet captured, or your server never received the webhook. Confirm auto-capture is enabled, check that your webhook endpoint returns HTTP 200 within 5 seconds, and reconcile using the
razorpay_payment_id.- (shared webhook, Late Auth, monitoring, rollback and logging deep-dives)
Is this integration guide useful?
ON THIS PAGE