Standard Checkout Integration Guide

Complete end-to-end guide to integrate Razorpay Standard Checkout. Create orders, verify payments, set up webhooks, capture funds and go live safely.


This is a complete, end-to-end guide to accept payments on your website using Razorpay Curlec Standard Checkout. It covers everything from your first test order to going live and monitoring payments in production.

Once you finish integrating Razorpay Curlec using this guide, you will be able to accept payments from your customers using Standard Checkout. To see how this compares with the other integration options, refer to the

.

What you are responsible for:

  • Creating a payment record (Order) before checkout.
  • Verifying the payment is genuine after checkout.
  • Setting up webhooks so your server knows when money arrives.
  • Capturing the payment so it settles to your account.

Razorpay Curlec handles the rest.

Handy Tips

If you use WooCommerce, Shopify, WordPress, Magento or another ecommerce platform, use our

instead of building this integration yourself.

Customer Your Frontend Your Server Razorpay
| | | |
| 1. Start checkout | | |
|-------------------->| 2. Create order | |
| |---------------------->| 3. POST /v1/orders |
| | |--------------------->|
| | | order_id |
| |<----------------------|<---------------------|
| 4. Pay (modal) | | |
|-------------------->| 5. checkout.js opens | |
| |------------------------------------------------>|
| | OTP / bank redirect / UPI approval |
|<------------------------------------------------------------------->|
| | 6. success (handler / callback_url) |
| | (payment_id, order_id, signature) |
| |---------------------->| 7. Verify signature |
| | | 8. Capture + fulfil |
| | |--------------------->|
| | | 9. Webhook: captured |
| | |<---------------------|
  1. Create an order on your server (Step 1) and pass the order_id to the browser.
  2. Open Checkout with checkout.js (Step 2). Razorpay Curlec handles the payment UI, OTP and redirects.
  3. Verify the signature returned after payment on your server (Step 3).
  4. Capture and confirm the payment with webhooks (Step 4).

Every payment moves through a set of states. The state tells you exactly when to deliver goods or services and when to hold back.

Watch Out: Authorised is not the same as paid

A payment in the authorized state is not yet in your account. You must capture it, or enable auto-capture, before the capture window expires. Uncaptured payments are automatically refunded by Razorpay Curlec. Enable auto-capture in Dashboard → Account & SettingsPayment Capture.

Make sure these are in place before you write any code. Skipping one of them blocks you later.

1. Razorpay Dashboard access

  • Log in at .
  • Complete KYC and business verification. This is required before you can go live and accept real payments.
  • Confirm the account is not restricted or under review.

2. Generate Test API Keys

API Keys are the credentials your code uses to talk to Razorpay Curlec. You need a Key ID and a Key Secret.

  1. Go to Account & SettingsAPI Keys in the Dashboard.
  2. Make sure you are in Test Mode using the toggle at the top of the Dashboard.
  3. Select Generate Key and note your Key ID (rzp_test_…) and Key Secret.

Watch Out: Never expose your Key Secret

The Key Secret is used only on your server. Never put it in frontend code, browser JavaScript or any public repository. Your Key ID is safe to use in frontend code.

3. Technical prerequisites

  • A server-side backend (Node.js, Python, Java, PHP, Ruby, Go or .NET) to call the Orders API.
  • HTTPS on your website. This is required for Live Mode.
  • A valid, unexpired SSL/TLS certificate on your server and webhook endpoint.
  • DNS propagated and resolving correctly for your webhook hostname.

Before you show the checkout form, your server must create an Order on Razorpay Curlec using the

. Think of it as a locked payment receipt. It records the amount and currency server-side so they cannot be changed by the time the customer pays.

Watch Out: Do not skip order creation

Razorpay Curlec auto-refunds any payment made without an order_id, so you will never receive the money. Never create Orders in browser-side JavaScript, as your Key Secret would be exposed to anyone who views your page source. Create a fresh Order for every unique payment attempt.

Keep these order rules in mind:

  • Orders are immutable. Once created, the amount and currency cannot be changed. If you need a different amount, create a new order.
  • Orders do not auto-expire, and passing expire_by is rejected by the API.
  • Control expiry by closing the checkout modal with the timeout option.

POST https://api.razorpay.com/v1/orders
Authentication: HTTP Basic Auth
Username: YOUR_KEY_ID
Password: YOUR_KEY_SECRET

Request parameters

Currency-specific minimum amounts

Watch Out: Maximum single transaction amount

The maximum single transaction amount is ₹5,00,000 (50,000,000 paise). For amounts above this, contact

to discuss enterprise limits.

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": "rcpt_001",
"notes": {
"customer_id": "cust_abc123",
"plan": "gold",
"source": "web_checkout"
},
"partial_payment": true,
"first_payment_min_amount": 23000,
"capture": "automatic"
}'

All Razorpay Curlec API endpoints return errors in the same structure. The code field identifies the error class, description gives a human-readable explanation and field, when present, identifies the parameter at fault.

{
"error": {
"code": "BAD_REQUEST_ERROR",
"description": "Order amount less than minimum amount allowed",
"source": "business",
"step": "payment_initiation",
"reason": "input_validation_failed",
"metadata": {},
"field": "amount"
}
}

Handy Tips: Save the order in your database

Save the order_id (for example, order_IluGWxBm9U8zJ8) linked to your internal order record. Also save the amount, currency and receipt for reconciliation. You will need the order_id in Step 2 and Step 3.

Notes on multi-attempt orders:

  • A customer can attempt payment multiple times against the same order_id, for example, if their first card is declined and they then try UPI.
  • The order.attempts field increments with each attempt.
  • You do not need to create a new order for a retry.
  • Create a new order only if the fulfilment scenario changes, such as a different amount or a different customer.

Once your server creates the order and sends the order_id to the browser, load Razorpay Curlec's checkout.js and open the payment modal. Razorpay Curlec handles the entire payment experience from here, including card entry, UPI, OTP and bank redirects.

<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<!-- Load this on every page where the Pay button appears. -->
<!-- Never self-host this script. Always load it from the Razorpay CDN. -->

There are two ways to open Checkout. Choose one based on how your app is built.

<button id="rzp-button1">Pay</button>
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
<script>
var options = {
"key": "YOUR_KEY_ID",
"amount": "50000",
"currency": "INR",
"name": "Acme Corp",
"description": "Gold Plan - Monthly Subscription",
"image": "https://example.com/your_logo.png",
"order_id": "order_9A33XWu170gUtm", // from Step 1
"callback_url": "https://yourserver.com/payment/callback",
"prefill": {
"name": "Customer Name",
"email": "customer@example.com",
"contact": "+919876543210"
},
"notes": { "address": "Your Office" },
"theme": { "color": "#3399cc" },
"modal": {
"confirm_close": true,
"escape": false,
"backdropclose": false,
"animation": true
},
"retry": { "enabled": true, "max_count": 4 }
};
var rzp1 = new Razorpay(options);
document.getElementById("rzp-button1").onclick = function(e) {
rzp1.open();
e.preventDefault();
}
</script>

Handy Tips: Two things that improve your conversion rate

  • Always prefill the customer's phone number (contact) with the country code, for example, +91XXXXXXXXXX. It pre-fills OTP fields and measurably reduces drop-off.
  • rzp1.open() must be called directly from a user action such as a button click. Browsers block programmatic popup opens.

Yes. Beyond the web checkout.js, Standard Checkout is available for native and cross-platform apps. Order creation and signature verification stay on your server and are identical across web and mobile.

When a payment succeeds, Razorpay Curlec returns three fields to the browser. Do not deliver goods or services yet. A malicious actor can forge these fields in the browser, so you must verify them on your server before you fulfil any order.

{
"razorpay_payment_id": "pay_29QQoUBi66xm2f",
"razorpay_order_id": "order_IluGWxBm9U8zJ8",
"razorpay_signature": "9ef4dffbfd84f1318..."
}

Watch Out: Do not fulfil the order yet

These three fields arrive in the browser, which you do not control. A malicious actor can send you fake values. Always verify the signature on your server before updating your database or delivering anything.

The signature is an HMAC-SHA256 hash of the order_id and payment_id, signed with your Key Secret. If it matches what Razorpay Curlec generated, the payment is authentic.

// Algorithm: HMAC-SHA256
// Input: order_id + "|" + razorpay_payment_id
// Key: YOUR_KEY_SECRET
// Compare: generated_signature == razorpay_signature
generated_signature = HMAC_SHA256(
key = YOUR_KEY_SECRET,
data = razorpay_order_id + "|" + razorpay_payment_id
)
if generated_signature == razorpay_signature:
# Payment is authentic -> fulfil order
else:
# Signature mismatch -> reject, do NOT fulfil
import hmac, hashlib, os
def verify_signature(order_id, payment_id, received_sig, key_secret):
body = f'{order_id}|{payment_id}'
expected_sig = hmac.new(
key_secret.encode(), body.encode(), hashlib.sha256
).hexdigest()
# hmac.compare_digest prevents timing attacks
return hmac.compare_digest(expected_sig, received_sig)
# In your Flask/Django view:
payment_id = request.json["razorpay_payment_id"]
received_sig = request.json["razorpay_signature"]
# CRITICAL: get order_id from YOUR database using the real Razorpay callback field
order = Order.objects.get(razorpay_order_id=request.json["razorpay_order_id"])
KEY_SECRET = os.environ.get('RAZORPAY_KEY_SECRET')
if verify_signature(order.razorpay_order_id, payment_id, received_sig, KEY_SECRET):
order.mark_paid()
else:
return JsonResponse({"error": "Signature mismatch"}, status=400)

If you prefer not to compute the HMAC yourself, every Razorpay Curlec server SDK ships a verification helper. 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: Three things that protect you

  1. Use crypto.timingSafeEqual() (Node.js) or hmac.compare_digest() (Python) to prevent timing attacks. Plain string equality (===) leaks information about how many characters match.
  2. Use the order_id from your own server's database, not the razorpay_order_id returned in the browser callback, which could be spoofed by a malicious user.
  3. Store razorpay_payment_id, razorpay_order_id and razorpay_signature in your database for audit trails and idempotency checks.

Signature verification confirms the payment is genuine, but a payment in the authorized state is not yet in your account. You need to capture it, and you need a reliable way to know when captured status is confirmed, even if the customer closes their browser tab immediately after paying. Webhooks are Razorpay Curlec's way of telling your server that a payment is done without relying on the browser.

  1. Log in to Dashboard → Account & SettingsPayment Capture.
  2. Select Change next to Automatic Capture.
  3. Select Automatic Capture and set the time window. The default is immediate.
  4. Select Save.

Handy Tips: How capture settings behave

  • Capture settings only work if you have integrated the Orders API. Payments created without an order_id do not respect capture settings.
  • Capture settings on the Orders API take precedence over Dashboard settings. Override per order by passing "capture": "automatic" or "capture": "manual" in the order creation request.
  • For manual capture, call the (POST /v1/payments/{payment_id}/capture) with the amount in paise before the capture window expires.

Configure webhooks so your server is notified the moment a payment completes, regardless of what happens in the customer's browser.

  1. Log in to Dashboard → Account & SettingsWebhooks.
  2. Select + Add New Webhook.
  3. Enter your publicly accessible HTTPS endpoint URL.
  4. Set a strong Webhook Secret of at least 32 random characters. You use this to validate incoming events.
  5. Subscribe to the minimum events below, then select Save.

Events to subscribe (minimum recommended)

Watch Out: Webhook delivery behaviour

  • Timeout: Razorpay Curlec waits a maximum of 5 seconds for your endpoint to respond. If it does not receive an HTTP 200 within 5 seconds, it marks the delivery as failed.
  • Retries: Failed deliveries are retried up to 24 times over a 24-hour period, roughly once per hour. After 24 attempts with no 200, the event is dropped.
  • Async pattern (mandatory): Your endpoint must return HTTP 200 immediately, before doing any processing. Enqueue the event body to a job queue (Redis, SQS or similar) and process it in a background worker. Never do database writes, API calls or email sending inline before returning 200.
  • Replay attacks: Each event includes a created_at timestamp. Reject events where created_at is more than 5 minutes in the past.
  • IP whitelisting (recommended): Razorpay Curlec sends webhooks from a fixed set of IPs. Whitelist these in your firewall for extra security. See for the current IP list.

Every webhook payload from Razorpay Curlec is signed. You must verify the X-Razorpay-Signature header on each incoming request, or anyone could send fake events to your endpoint.

Razorpay Curlec provides a signature validation helper in every

. Pass the raw request body, the X-Razorpay-Signature header value and your webhook secret to the helper for your stack:

/* Java SDK: https://github.com/razorpay/razorpay-java */
Utils.verifyWebhookSignature(webhookBody, webhookSignature, webhookSecret);

The signature check is only the first step. In production, verify the signature synchronously, return 200 immediately, then process the event in the background so a slow handler does not trigger Razorpay Curlec's retries. The following Node.js (Express) pattern shows the full flow, including a replay-attack guard and idempotent processing:

// Express middleware: raw body required for HMAC
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
const received = req.headers['x-razorpay-signature'];
// 1. Verify signature FIRST (fast, synchronous)
try {
Razorpay.validateWebhookSignature(req.body.toString(), received, secret);
} catch (err) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// 2. Replay-attack guard: reject stale events
const eventAge = Date.now() / 1000 - event.created_at;
if (eventAge > 300) { // older than 5 minutes
return res.status(200).send('Stale event ignored'); // still return 200
}
// 3. Return 200 IMMEDIATELY, before any heavy processing
res.status(200).json({ received: true });
// 4. Enqueue for background processing (fire-and-forget with error logging).
// Do NOT await here: the response is already committed, so an unhandled throw
// would make Express attempt a second response write (headers already sent).
jobQueue.enqueue('process_razorpay_event', event).catch(err => {
console.error('Failed to enqueue webhook event, manual replay required', {
eventId: event.id,
error: err.message,
});
// Alert ops: Razorpay will NOT retry (200 was returned), so this event is lost.
});
});
// Background worker (runs outside the request)
async function processRazorpayEvent(event) {
const paymentId = event.payload.payment?.entity?.id;
// Idempotency: atomic insert-if-not-exists, relies on a unique constraint on event.id.
// A single operation leaves no window for a race between the check and the insert.
try {
await db.processedEvents.insert(event.id);
} catch (err) {
// A unique constraint violation means another worker already claimed this event.
if (err.code === 'UNIQUE_VIOLATION' || err.code === '23505') return;
throw err; // unexpected error, re-throw
}
switch (event.event) {
case 'payment.captured':
await db.orders.markPaid(paymentId);
await emailService.sendConfirmation(paymentId);
break;
case 'payment.failed':
await db.orders.markFailed(paymentId);
await emailService.sendFailureAlert(paymentId);
break;
case 'refund.failed':
await alertOpsTeam(paymentId, 'refund_failed');
break;
case 'payment.dispute.created':
await db.orders.flagDisputed(paymentId);
await alertOpsTeam(paymentId, 'dispute_raised');
break;
}
}

Watch Out: Pass the raw body to HMAC

Pass the raw request body (bytes) to the HMAC function. Do not parse the JSON first. Parsing changes the byte sequence and causes a signature mismatch, even if the content is identical. In Express, use express.raw(), not express.json(), on the webhook route.

Late Auth occurs when a payment is authorised by the bank after your order session has already expired on your side. For example, the customer starts payment, your order times out after 10 minutes, but the bank approves the transaction 12 minutes later. Without handling this, the customer has been debited but you have not fulfilled, which leads to a dispute.

How Late Auth happens

  • The customer starts a payment (UPI, netbanking or card with OTP).
  • Your server-side order expires, for example, you mark it abandoned after 10 minutes.
  • The bank processes slowly and authorises the payment after your timeout.
  • Razorpay Curlec sends a payment.authorized webhook to your endpoint.
  • Your system does not recognise the order as active, and the customer is charged with no fulfilment.

All testing is done in Test Mode using test API keys, so no real money moves. Each test scenario corresponds to a real situation that will cost you money or customer trust if it is not handled correctly.

Handy Tips: Test Mode setup

Ensure your Dashboard is in Test Mode using the toggle in the top bar. Use only Test Mode API Keys (rzp_test_XXXXXXXXXXXXXXXX) in your code during testing.

Handy Tips: OTP rules in Test Mode

  • A 4-digit OTP always succeeds (for example, 1234 or 0000).
  • A 5-digit OTP starting with 1 always fails (for example, 12345).
  • A 6 to 10 digit OTP always succeeds.
  • For netbanking, select any bank and you will see Success and Failure buttons on the mock bank page.
  • For wallets, any test amount works. Select Pay on the mock wallet page.

Run every scenario below in Test Mode before moving to go-live.

All Tier 1 items must be verified before you switch to Live API keys. This checklist is your merchant audit record.

Tier 1.1: Account and API Keys

  • KYC documents submitted and approved on the Dashboard.
  • Business category and website URL correctly set in the Dashboard.
  • Live Mode API Keys generated from the Dashboard, not the test keys.
  • Test API keys removed from all production environments and config files.
  • KEY_SECRET stored in environment variables, not hardcoded in source code.
  • Source code does not contain any API secrets, verified via git grep or equivalent.

Tier 1.2: Integration correctness

  • Every payment attempt creates a fresh Order via the Orders API.
  • order_id is passed correctly to the Checkout options.
  • Amount in the Order API matches the amount in the Checkout options exactly, both in paise.
  • Currency in the Order API matches the currency in the Checkout options.
  • checkout.js loaded from the Razorpay Curlec CDN, not self-hosted.
  • Checkout opens only on a user action (button click), not auto-open on page load.
  • Customer prefill (name, email, contact with country code) populated.

Tier 1.3: Security (non-negotiable)

  • Signature verification implemented server-side using HMAC-SHA256.
  • order_id used for verification comes from the server DB, not the client callback.
  • Order fulfilled only after signature verification passes.
  • Timing-safe string comparison used (timingSafeEqual or hmac.compare_digest).
  • razorpay_payment_id stored in the DB for deduplication and idempotency.
  • API Key Secret never exposed to the frontend or logged in application logs.
  • HTTPS enforced on all payment pages and callback or webhook URLs.
  • SSL certificate valid and not expiring within 30 days.

Tier 1.4: Capture and webhooks

Capture, so you actually get the money:

  • Auto-capture configured in the Dashboard, or the Orders API capture parameter set.
  • All authorised payments will be captured within the capture window (confirmed).
  • Goods or services not delivered before the payment reaches captured state.
  • Settlement schedule confirmed with the merchant.

Webhooks, so your server knows when you have been paid:

  • Webhook URL configured in the Dashboard (Live Mode), not just Test Mode.
  • Webhook secret configured and signature verified on every incoming event.
  • payment.captured and payment.failed events subscribed and handled.
  • refund.failed and payment.dispute.created subscribed and handled.
  • Webhook endpoint returns HTTP 200 within 5 seconds. Heavy processing is async.
  • Duplicate webhook events handled idempotently (event.id stored and checked).
  • Replay attack guard: events older than 5 minutes are discarded.
  • DNS resolves correctly for the webhook URL and the SSL cert is valid.

Tier 1.5: Go-live execution

  • All Test Mode scenarios from Step 5 pass with test keys.
  • Live API keys generated from the Dashboard (Live Mode).
  • Test API keys replaced with Live keys in all environments.
  • Environment variables updated, not hardcoded keys.
  • A live test transaction completed and verified in the Dashboard.
  • Payment visible as Captured in the Live Dashboard.
  • Webhook received and processed for the live test transaction.
  • Rollback plan documented and tested (see the Appendix).

Tier 2.1: Operational hardening (within 30 days)

  • Payment failure shown to the customer with a clear message and retry option.
  • Checkout modal dismiss handled gracefully.
  • Loading states shown while the Order is being created server-side.
  • Double-submit prevention on the Pay button (disabled after first click).
  • Mobile responsiveness of the payment page verified.
  • Timeout handling implemented if the checkout session expires.
  • Late Auth scenario handled (payment authorised after the order expires).
  • Business logo set in the image parameter and theme colour matches brand guidelines.

Handy Tips: API rate limits

The Orders API and Payments API are limited to roughly 100 requests per minute per key. Webhook deliveries are retried up to 24 times over 24 hours. Contact Razorpay Curlec to request a higher limit before load testing, and use a dedicated load test key rather than production keys.

Your integration is live. Watch these metrics so you can act before small problems become customer-facing ones.

Daily operations checklist

  • Check Dashboard → Payments for any payments stuck in the authorized state.
  • Review webhook delivery logs for failed retries.
  • Check for any payment.dispute.created events and respond within 7 days.
  • Verify settlement amounts match expected totals.
  • Monitor refund.failed events. Each requires manual follow-up.

Fill this in before switching to Live keys. If something goes wrong, you need to act in seconds, not minutes.

Log enough to debug payments, but not so much that you create a PCI compliance risk.

logger.info({
event: "payment_verified",
order_id: order.razorpayOrderId,
payment_id: razorpay_payment_id,
result: "success",
duration_ms: Date.now() - startTime
// DO NOT include: signature, key_secret, card_number, cvv
});

Do I need to create an order for every payment?

Yes. Create a fresh Order via the Orders API for every unique payment attempt. Razorpay Curlec auto-refunds any payment made without an order_id, so you would never receive the money. You can reuse the same order_id if a customer retries after a failure, but create a new order if the amount or customer changes.

What is the difference between authorized and captured?

authorized means the customer has paid and the funds are held by Razorpay Curlec, but the money is not yet in your account. captured means the money is confirmed and queued for settlement. Deliver goods or services only after the payment reaches captured. Capture an authorised payment, or enable auto-capture, before the capture window expires, or it is auto-refunded.

Why must I verify the payment signature on the server?

The three fields returned after a successful payment (razorpay_payment_id, razorpay_order_id and razorpay_signature) arrive in the browser, which you do not control. A malicious actor can forge them. Verifying the HMAC-SHA256 signature on your server with your Key Secret proves the payment is genuine before you fulfil the order. Skipping this is the most common cause of fraudulent orders.

Why should I use webhooks if I already get a browser callback?

The browser callback does not fire if the customer closes the tab immediately after paying. Webhooks notify your server independently of the browser, so your fulfilment logic works reliably. Use webhooks as the source of truth for fulfilment and never rely on the browser callback alone.

What is Late Auth and how do I handle it?

Late Auth happens when the bank authorises a payment after your order session has expired. The customer is charged but your system no longer recognises the order. Handle it in your payment.authorized webhook: if the order is still active, fulfil normally; if it is expired, cancelled or already fulfilled by another payment, refund the customer immediately and notify them. See Step 4.5 for the handling pattern.

How do I test the integration without real money?

Switch your Dashboard to Test Mode and use Test Mode API Keys (rzp_test_…). Use the test cards and UPI VPAs in Step 5 to simulate success, failure, decline and pending scenarios. No real money moves in Test Mode. Run the full test checklist before switching to Live keys.

My customer paid but the money never arrived. What went wrong?

The two most common causes are: the payment was not captured before the capture window expired, so it was auto-refunded; or the order was created without an order_id being passed to Checkout. Always create the Order first, pass the order_id and enable auto-capture. Check Dashboard → Payments for payments stuck in the authorized state.


Is this integration guide useful?


standard checkout
payment gateway integration
razorpay checkout
verify payment signature
payment webhooks