Skip to main content
Nozle uses Razorpay to collect INR payments through the Razorpay checkout modal or a Razorpay payment link. Each Nozle organization connects its own Razorpay account, so your customers pay into your Razorpay account, not Nozle’s. Nozle holds no global Razorpay credentials.

What you need

  • A Razorpay account and an API key. The Key ID starts with rzp_test_ for test mode or rzp_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 returns payment_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:
  1. Copy the Webhook URL shown on the connection in Nozle. It has this form:
  1. Use the same webhook secret you entered in Nozle.
  2. 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-Id header 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).
A valid request is queued and answered with 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

  1. Customer selects a plan in your app.
  2. Your backend derives the customer and calls Nozle checkout with an sk_ key.
  3. Nozle computes the exact invoice or proration, keeping the current plan active.
  4. If money is due, Nozle creates a Razorpay order with automatic capture and returns a razorpay result.
  5. The browser opens the Razorpay checkout modal with the order.
  6. Razorpay calls the modal’s handler with the payment fields.
  7. 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.
  8. Your backend polls checkout status until status and fulfillment_status are both succeeded.
The 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_id is the public Key ID. The key secret and webhook secret never appear in a response.
  • amount_cents is in paise.
  • A paid upgrade returns plan_upgrade_intent_id instead of invoice_id.
  • An Entity checkout adds external_entity_id and external_subscription_id. Single-Entity and bulk Entity checkouts both return a razorpay result.
  • A mandate checkout also returns customer_id (a Razorpay customer ID), recurring: true and mandate_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_pending while 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 answers 422 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": "..."}}. Its razorpay result follows from checkout status once the order is ready, in the nested checkout. Replaying the same bulk request with the same Idempotency-Key returns type: "razorpay" with checkout_id, key_id and order_id as soon as the order exists.
  • In a processing result, status is an internal operation status, for example ready for an unpaid order that has expired. Read the top-level status of checkout status for the state of the checkout.

Create, verify and poll

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

Load https://checkout.razorpay.com/v1/checkout.js, then open the modal with the values from the razorpay result:
For a mandate checkout, also pass 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 with payment: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_signature is the HMAC-SHA256 of order_id|payment_id computed with your key secret. A mismatch returns 422 validation_errors with invalid_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 authorized payment moves the checkout to processing and waits for capture.
A browser success callback alone never activates a plan.

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 same Idempotency-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. 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 an rzp.io URL in a payment_url field. Two calls return it:
  • POST https://api.nozle.app/core/api/v1/invoices/{id}/payment_url returns {"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" and payment_url.
Neither result has a checkout ID, so there is nothing to poll. Redirect the customer to 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 as payment_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:
  1. Turn on Enable recurring payments and set the mandate maximum on the connection. Your Razorpay account must have UPI AutoPay enabled.
  2. Create the checkout for a subscription invoice with registerMandate: true (register_mandate in HTTP and Python). The customer needs a name, email and phone number.
  3. Open the modal with customer_id and recurring: '1'. Nozle creates the order as a UPI authorization with the connection’s maximum amount.
  4. Razorpay sends token.confirmed. Only then does the mandate become usable, and only for the subscriptions covered by that registration.
For a renewal, Nozle uses a confirmed, unexpired UPI mandate that covers every subscription on the invoice and whose maximum is at least the amount due. If the debit is rejected, the invoice returns to needing an interactive payment and Nozle sends the invoice payment-failure notification, once per failed attempt. Retry payment on that invoice (dashboard, API or dunning) debits the confirmed mandate again right away with a new order, instead of doing nothing for 24 hours. A retry that cannot debit sends no further notification. Mandate registration errors:

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 (HTTP 429), 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 pending until Razorpay reports processed. If Razorpay reports failed, Nozle sends the credit_note.refund_failure webhook.
  • 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 1111 is rejected by many Razorpay test accounts, which accept domestic cards only. Use a domestic test card such as the Visa 4386 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.