What you need
- A Razorpay account and an API key. The Key ID starts with
rzp_test_for test mode orrzp_live_for live mode. - The key secret for that Key ID.
- A webhook secret that you choose. You set the same value in Nozle and in Razorpay.
- Plans, invoices and top-up packages priced in INR. Razorpay checkout supports only INR.
- UPI AutoPay enabled on your Razorpay account, only if you want recurring mandates.
- A Nozle public API URL that Razorpay can reach.
Setup
Connect Razorpay in the Nozle dashboard under Settings > Billing settings > Integrations. In the Razorpay group choose Connect. Once a connection exists, the group offers Add another, Edit and Disconnect. The form asks for:
The dashboard keeps Connect disabled until the Key ID matches
rzp_test_ or rzp_live_, and, with recurring payments on, until the mandate maximum is a whole number of at least 100. Nozle checks the credentials with one authenticated Razorpay call when you connect or change the key secret. Credentials Razorpay rejects fail with 422 third_party_error, and nothing is saved.
Nozle reads the mode from the Key ID prefix and shows it on the connection together with the webhook URL. To use a different Key ID, for example to move from test to live, create a new connection. You can add several connections to one organization.
To rotate the webhook secret, edit the connection and enter the new value. Nozle keeps accepting the previous secret for 25 hours, so deliveries signed with the old secret still verify.
Assign the connection to customers
A customer pays through Razorpay only when that customer uses a Razorpay connection. Select it in the customer’s Payment provider setting in the dashboard, which lists connections as name and code. A customer who is not assigned to Razorpay goes through the Stripe path, and checkout returnspayment_provider_required if the customer has no assigned Stripe connection either. If the customer’s connection code matches no Razorpay connection, checkout returns 404 payment_provider_not_found.
Nozle links the customer to the connection when you create the customer in the dashboard or assign the connection. A customer without that link, for example one assigned before this change, is linked on its next payment attempt, so credit top-ups, invoice payment URLs and the payment links of overdue-invoice reminders work without a plan checkout first. Nozle creates the customer in Razorpay when it needs one, for mandate registration, which needs the customer’s name, email and phone number.
Customer sync reuses a buyer who already exists in your Razorpay account instead of failing with “Customer already exists”, and links that Razorpay customer. A request Razorpay rejected is not sent again while the customer’s details are unchanged; after you correct the phone number or email, the next sync sends a new request. A request whose outcome is unknown is reconciled automatically. The phone number is sent as E.164 digits.
Once a customer has started a Razorpay checkout, it stays linked to that connection. Switching the customer to another Razorpay connection then fails with 422 razorpay_customer_connection_cannot_change, through the customer API (POST /customers) and the dashboard alike. Nothing is saved: the customer keeps its connection code. Create a new customer for the other connection instead.
To assign the connection from code, send billing_configuration.payment_provider: "razorpay" and payment_provider_code when you create or update the customer. Nozle keeps the assignment even without sync_with_provider.
What Nozle handles
Webhook setup
Nozle does not register the Razorpay webhook for you. Add it in the Razorpay dashboard:- Copy the Webhook URL shown on the connection in Nozle. It has this form:
- Use the same webhook secret you entered in Nozle.
- Enable these events:
Nozle computes an HMAC-SHA256 of the raw request body with the webhook secret and compares it with the
X-Razorpay-Signature header. A request is answered with 400 when:
- the provider ID in the URL is unknown or is not a UUID;
- the signature is not 64 lowercase hexadecimal characters, or does not match the body;
- the
X-Razorpay-Event-Idheader is missing or longer than 255 bytes; - the body is not valid JSON or has no
event; or - an event ID that Nozle already received arrives with a different body (
webhook_event_payload_conflict).
200. Nozle de-duplicates on the event ID, so a repeated event never settles twice.
If the webhook is missing or blocked, Razorpay can capture the payment but Nozle is not told. A captured order then settles only when your backend calls verify or status, or when the reconciliation job that runs every five minutes reaches it. Dispute and mandate events are never received.
Checkout flow
- Customer selects a plan in your app.
- Your backend derives the customer and calls Nozle checkout with an
sk_key. - Nozle computes the exact invoice or proration, keeping the current plan active.
- If money is due, Nozle creates a Razorpay order with automatic capture and returns a
razorpayresult. - The browser opens the Razorpay checkout modal with the order.
- Razorpay calls the modal’s
handlerwith the payment fields. - Your backend sends those fields to Nozle verify. Nozle checks the signature, fetches the payment from Razorpay, and settles it only if it is
captured. - Your backend polls checkout status until
statusandfulfillment_statusare bothsucceeded.
payment.captured and order.paid webhooks settle the same order independently, so a closed browser tab does not lose a paid checkout.
If prepaid credits fully cover the invoice, Nozle returns a completed checkout and Razorpay is not called. Nozle passes no return URL to Razorpay, but the returnUrl you send must still be an allowlisted HTTPS origin, as for any checkout.
The checkout result
key_idis the public Key ID. The key secret and webhook secret never appear in a response.amount_centsis in paise.- A paid upgrade returns
plan_upgrade_intent_idinstead ofinvoice_id. - An Entity checkout adds
external_entity_idandexternal_subscription_id. Single-Entity and bulk Entity checkouts both return arazorpayresult. - A mandate checkout also returns
customer_id(a Razorpay customer ID),recurring: trueandmandate_max_amount_cents. The customer needs a name, email and phone number. - An invoice checkout order stays open for 24 hours. A paid upgrade stays open for 35 minutes. A new-subscription, single Entity or bulk Entity checkout order stays open for 23 hours (
expires_at). - An upgrade is refused with
409 subscription_change_pendingwhile the subscription has a scheduled cancellation or plan change. Cancelling at period end or scheduling a plan change expires an open upgrade order. - When an upgrade’s 35 minutes pass, Nozle fetches the order’s payments before it reports the upgrade expired. A payment captured in time is settled and the upgrade is applied, even if its webhook has not arrived. A payment still in progress, or a Razorpay error, answers
409 plan_upgrade_checkout_in_progress. An order without a payment expires, and a new checkout creates a new order. Once a late payment is applied, a new checkout of the same plan answers422 plan_already_active. See Upgrade checkout that ran out of time. - If the order is not ready yet, or has expired, the result is
{"type": "processing", "checkout_id", "status"}. Recover it with checkout status rather than starting another checkout. Some checkouts return this shape first, because Nozle submits the order to Razorpay in the background: a bulk Entity checkout answers{"entity_subscription_checkout": {"type": "processing", "status": "open", "client_secret": null, "checkout_id": "..."}}. Itsrazorpayresult follows from checkout status once the order is ready, in the nestedcheckout. Replaying the same bulk request with the sameIdempotency-Keyreturnstype: "razorpay"withcheckout_id,key_idandorder_idas soon as the order exists. - In a
processingresult,statusis an internal operation status, for examplereadyfor an unpaid order that has expired. Read the top-levelstatusof checkout status for the state of the checkout.
Create, verify and poll
- Node.js
- Python
- Go
verifyCheckout() posts the fields to Nozle. The SDK does not check the signature locally. Verify and status calls need an sk_ key, so they run only in your backend. Bind each checkout_id to the buyer who started it before you call Nozle. Pass both scope fields to tie the call to one customer and subscription.
Open the modal in the browser
Loadhttps://checkout.razorpay.com/v1/checkout.js, then open the modal with the values from the razorpay result:
customer_id: checkout.customer_id and recurring: '1'.
React
With the React SDK,CheckoutButton runs this browser flow for you. It loads the Razorpay script, opens the modal, calls your verifyCheckout callback, then polls your getCheckoutStatus callback up to 30 times, one second apart. Both callbacks call your authenticated merchant backend:
CheckoutButton rejects a Razorpay result without checkout_id, key_id and order_id, and throws when BillingProvider has no verifyCheckout. See the React components and hooks.
HTTP
The SDKs call these Engine endpoints. All three need a secret key withpayment:write (create, verify) or payment:read (status):
Add
customer_id and subscription_id query parameters together to scope verify and status to one customer and subscription. An unknown checkout ID returns 404 checkout_session_not_found.
How Nozle verifies a payment
- Verify call. Nozle checks that the order ID is this checkout’s order and that
razorpay_signatureis the HMAC-SHA256 oforder_id|payment_idcomputed with your key secret. A mismatch returns422 validation_errorswithinvalid_checkout_signature. - Provider fetch. Nozle then fetches the payment from Razorpay with your connection’s credentials. It never trusts the browser fields or a webhook body alone.
- Settlement checks. Nozle settles only a payment that is
captured, belongs to the order or link, and matches the checkout’s amount and currency. The invoice must still be payable, not expired, and the customer must still use the same connection. - Authorized is not paid. An
authorizedpayment moves the checkout toprocessingand waits for capture.
Status
GET /checkout/{checkout_id} and verify return the checkout status:
Show success only when both are
succeeded, then reload the subscription. The response also carries checkout_id, provider, amount_cents, currency and invoice_id, plus a nested checkout result while the checkout is awaiting_payment, so a client can resume after a reload. The nested checkout can also appear while the top-level status is expired: for an unpaid order that has passed its expires_at, it is {"type": "processing", "status": "ready", ...}, not a razorpay result. ready is an internal operation status. Do not reopen the modal for it.
needs_review means a captured payment could not settle its invoice. Causes are a changed amount or currency, a payment made after the checkout expired, an invoice that is no longer payable, a changed provider connection, or another payment in progress. A capture that is held behind another payment in progress also moves the checkout to needs_review, so checkout status no longer keeps reporting awaiting_payment or expired for it. Nozle grants no plan, credits or successful payment for it. Check the payment in the Razorpay dashboard and refund or reconcile it deliberately. Nozle cannot cancel a Razorpay order, so an expired order can still be paid late.
Expiry is judged by when the customer paid, not by when Nozle processed the payment. An invoice order or payment-request link paid before its expiry settles the invoice even when the webhook or the verify call arrives later, and even when reconciliation has already closed the checkout. Only a payment that Razorpay created after the expiry goes to needs_review.
If the customer closes or fails the modal, a one-time order stays open until it expires, and reopening the modal on the same order works. Checkout status returns the order as the nested checkout while the status is awaiting_payment, so store the checkout you received, or fetch it again from status, and open the modal with it.
The React SDK reports a modal payment.failed event as “Payment failed. You can retry checkout.” The retry button calls your createCheckout callback again, which reopens the same order; see Retry a new-subscription checkout.
Retry a new-subscription checkout
A new-subscription checkout leaves an incomplete subscription behind until the first order is paid, for example when the customer closed the modal or the page reloaded. Calling checkout again for the same customer and plan while that subscription is incomplete reopens the first checkout for the same invoice instead of failing, with or without the sameIdempotency-Key. The reopened checkout is the same order. Checkout status with the checkout_id of the first response, or an invoice checkout for its invoice_id, also returns it. Asking for a different plan replaces the abandoned checkout: the old checkout expires and the new checkout is returned. 409 subscription_checkout_in_progress is returned only while Razorpay is still processing a payment for the old checkout, or when that checkout turns out to be paid.
An unpaid new-subscription checkout expires after 23 hours: the subscription is canceled with cancelation_reason: timeout (subscription.canceled webhook), the invoice is closed, the coupons, credit-note credits and wallet credits it used are returned, and its open order is expired. See Unpaid checkout expiry.
Payment links
Nozle uses a Razorpay payment link where the customer is not inside your checkout modal: an invoice payment URL, a credit top-up purchase, and the link for overdue invoices. Nozle asks Razorpay not to send SMS or email for the link, and the link expires after 24 hours. The link is anrzp.io URL in a payment_url field. Two calls return it:
POST https://api.nozle.app/core/api/v1/invoices/{id}/payment_urlreturns{"invoice_payment_details": {"customer_id", "external_customer_id", "payment_provider", "invoice_id", "payment_url"}}.- Create Credit Top-up Purchase returns the purchase with
payment_status,payment_provider: "razorpay"andpayment_url.
payment_url. The payment_link.paid webhook settles it, Payment links are not verified with the verify call, which is for orders.
POST /checkout never returns type: "hosted" for Razorpay: it returns an order. Only the deprecated POST https://api.nozle.app/engine/api/v1/credits/purchase returns type: "hosted" with a payment_url. That endpoint requires an Idempotency-Key header and a currency field (the ISO 4217 code of the top-up package, for example USD), and answers 400 with the code idempotency_key_required or currency_required without them; a retry with the same key returns the first purchase. Use Create Credit Top-up Purchase (POST /credit-top-up-purchases) instead. The SDK result types still declare hosted.
Credit top-ups
A credit top-up for a Razorpay customer returns a Razorpay payment link aspayment_url, and payment_provider is razorpay. Navigate to it. Credits are granted once the top-up invoice is fully paid and the credit signal is delivered, so capture and the credit grant can finish at different times. A captured Razorpay payment pays it; so does a manual payment for the full amount, a payment request, or marking the invoice paid. The top-up package must be priced in INR, and its currency must match the customer’s currency. See Paid Credit Top-ups and Create Credit Top-up Purchase.
Recurring payments with UPI AutoPay
Renewals are charged automatically only for a confirmed UPI AutoPay mandate. Otherwise Nozle does not charge, and the customer pays the invoice through a new checkout or payment link. To register a mandate:- Turn on Enable recurring payments and set the mandate maximum on the connection. Your Razorpay account must have UPI AutoPay enabled.
- Create the checkout for a subscription invoice with
registerMandate: true(register_mandatein HTTP and Python). The customer needs a name, email and phone number. - Open the modal with
customer_idandrecurring: '1'. Nozle creates the order as a UPI authorization with the connection’s maximum amount. - Razorpay sends
token.confirmed. Only then does the mandate become usable, and only for the subscriptions covered by that registration.
Reconciliation and rate limits
Nozle checks the status of open Razorpay orders, payment links, debits and refunds with your connection’s key. It checks operations from the last hour every few minutes and older ones once an hour. A checkout that nobody paid stops being checked once its expiry plus one hour has passed, or once its invoice or upgrade can no longer be paid through it. When Razorpay rate-limits your key (HTTP429), Nozle sends the order, link, debit, refund or customer sync again after the Retry-After delay (60 seconds when Razorpay sends none), instead of failing the refund or the collection or falling back to interactive payment.
Refunds and disputes
- Refunds. A credit-note refund is sent to Razorpay against the captured payment at normal speed. It stays
pendinguntil Razorpay reportsprocessed. If Razorpay reportsfailed, Nozle sends thecredit_note.refund_failurewebhook. - Disputes. On
payment.dispute.lost, Nozle fetches the dispute from Razorpay, validates it against the payment, and marks the invoice’s dispute lost. A closed or won dispute is not treated as lost.
Errors
Checkout errors that are specific to Razorpay. See Create checkout for the full list.Limits
- INR only. Cross-currency collection is not enabled.
- Cards and UPI through the interactive checkout, and UPI AutoPay mandates for subscription invoices.
- Not enabled: standalone zero-value or trial mandates, hosted mandate-registration links, recurring cards and arbitrary grouped automatic debits.
- Overdue invoices can be grouped into one payment link. Credit top-up and prepaid-credit invoices cannot be grouped and are collected individually.
- The Key ID of a connection cannot change.
Troubleshooting
Testing with a test Key ID
Test the flows you use with a test Key ID connection before you connect live keys. These points came up in test-mode runs of the checkout, with a signed webhook, on a local stack:- The test card
4111 1111 1111 1111is rejected by many Razorpay test accounts, which accept domestic cards only. Use a domestic test card such as the Visa4386 2894 0766 0153, and choose Success on the bank page. - UPI collect and UPI AutoPay are not available in every test account, so test a mandate checkout only in an account that has them.
- The Razorpay modal rejects obviously fake phone numbers. Enter a plausible number.