Skip to main content
Nozle collects plan payments through the payment provider connected to your organization: Stripe or Razorpay. The flow is the same for both:
  1. Customer selects a plan in your application
  2. The React SDK calls your authenticated merchant callback with { planCode, returnUrl }
  3. Your backend derives the customer, validates the plan and return origin, and calls Nozle with sk_
  4. Nozle keeps the current plan active and computes the exact invoice or proration
  5. If money remains due, Nozle returns a provider result: an embedded Stripe Checkout Session secret, or a Razorpay order
  6. Customer completes payment in the provider’s UI
  7. The provider confirms the payment to Nozle (a signed webhook, and for Razorpay also your verify call)
  8. Nozle applies the paid plan change only after verified success
Plan checkout is payment-gated: a paid plan change is not applied until the payment provider confirms payment. If prepaid credits reduce the exact amount due to zero, Nozle applies the change transactionally and returns type: "completed" without creating a provider session.
A customer whose provider is not connected cannot pay: checkout returns payment_provider_required. Checkout charges only through the connection assigned to the customer and never picks another one; a customer with no assigned connection also gets payment_provider_required. Assign the connection in the dashboard, or send billing_configuration.payment_provider and payment_provider_code when you create the customer through the API; see the Razorpay and Stripe guides. At checkout Nozle creates the customer in Stripe when it is missing, and returns 422 stripe_customer_required if it cannot.

Stripe

Plan and invoice checkout always create an embedded Stripe Checkout Session and return its cs_ client secret. They never return a Stripe-hosted URL. Credit top-ups are the exception: they return a Stripe-hosted checkout.stripe.com payment_url that is valid for 24 hours. Mount the secret with Stripe Embedded Checkout (the React Checkout component does this for a cs_ secret).

Required Stripe webhook setup

When you connect a Stripe account, Nozle registers a Stripe webhook endpoint for that account and stores Stripe’s webhook signing secret. Your Stripe account must allow webhook endpoint creation. The webhook target is derived from your public Nozle API URL:
Stripe events Nozle handles:
  • payment_intent.succeeded
  • payment_intent.payment_failed
  • payment_intent.canceled
  • setup_intent.succeeded
  • payment_method.detached (received, but it does not clear the stored payment method today)
  • customer.updated
  • charge.refund.updated
  • charge.dispute.closed
Nozle subscribes to these eight events only. Without this webhook, Stripe may collect payment but Nozle is not told. Checkout status of a paid upgrade reads the Checkout Session from Stripe and reports processing once it is complete, but it never settles a payment, so only the webhook does. See the Stripe integration for setup, flows and limits. A new-subscription Stripe checkout returns no checkout ID to poll. After Embedded Checkout completes, reload the subscription or wait for your subscription.started webhook. A paid upgrade returns plan_upgrade_intent_id; poll its status with GET /checkout/{id} and the subscription scope. Retrying a new-subscription checkout for the same plan while its subscription is still incomplete reopens the first checkout. A different plan replaces the abandoned checkout and returns the new one; 409 subscription_checkout_in_progress remains only while the provider is still processing a payment for the old checkout. An unpaid plan or single Entity checkout expires after 23 hours: the subscription is canceled with cancelation_reason: timeout, the invoice is closed and the credits it used are returned. See Unpaid checkout expiry.

Razorpay

Connect Razorpay in the dashboard with its key ID, key secret and webhook secret. Optional settings enable recurring mandates (with a maximum mandate amount). Razorpay supports only INR. Nozle does not register the Razorpay webhook for you: copy the webhook URL Nozle shows for the connection into your Razorpay dashboard, with the same webhook secret. See the Razorpay integration for setup, events and limits.
Nozle checks the X-Razorpay-Signature header against the webhook secret and de-duplicates on X-Razorpay-Event-Id. On payment.authorized, payment.captured, payment.failed, order.paid and payment_link.paid, Nozle fetches the payment from Razorpay and settles the checkout once the payment is captured. Refund, mandate and dispute events are also handled. A Razorpay checkout runs in four steps:
  1. Create. Checkout returns {"type": "razorpay", "checkout_id", "key_id", "order_id", "amount_cents", "currency", "expires_at", "invoice_id"}. A paid upgrade has plan_upgrade_intent_id instead of invoice_id. A mandate checkout (register_mandate) also has customer_id, recurring: true and mandate_max_amount_cents. If the order is not ready yet, or has expired, the result is {"type": "processing", "checkout_id", "status"} instead. A bulk Entity checkout first answers {"entity_subscription_checkout": {"type": "processing", "status": "open", "checkout_id": "..."}}. A processing result carries an internal operation status such as ready, not the state of the checkout.
  2. Pay. Load https://checkout.razorpay.com/v1/checkout.js and open new window.Razorpay({ key: key_id, order_id, handler }).
  3. Verify. Send the three fields from the handler to your backend, which calls POST /checkout/{checkout_id}/verify with razorpay_order_id, razorpay_payment_id and razorpay_signature. A missing field returns 400. Nozle checks that the order matches the checkout and that the signature is the HMAC-SHA256 of order_id|payment_id with your key secret; a mismatch returns 422 validation_errors with invalid_checkout_signature. Nozle then fetches the payment from Razorpay and settles only a captured payment. The response is the checkout status.
  4. Poll. Call GET /checkout/{checkout_id} until status and fulfillment_status are both succeeded. Each call reconciles with Razorpay.
The nested checkout of the status response is present while the checkout is awaiting_payment and also for an expired unpaid order. For the expired order it is {"type": "processing", "status": "ready"}, so do not reopen the modal for it. A status call without a customer and subscription scope needs a secret key; an unknown ID returns 404 checkout_session_not_found. The React CheckoutButton runs steps 2 to 4 for you when BillingProvider has verifyCheckout and getCheckoutStatus callbacks; it polls 30 times, one second apart. To retry a new-subscription checkout, call checkout again for the same plan: while the first order is unpaid it returns that same order. The React “Payment failed. You can retry checkout.” button does this. See Retry a new-subscription checkout. POST /checkout never returns type: "hosted" for Razorpay. A Razorpay payment link comes from an invoice payment URL (POST /invoices/{id}/payment_url) or a credit top-up purchase, as an rzp.io payment_url with no checkout ID to poll. Redirect the customer to it; the payment_link.paid webhook settles it.

API shape

Plan checkout:
Free plans and paid upgrades fully funded by prepaid credits return a completed response instead. Downgrades and other cycle-end transitions return a scheduled response from the same /checkout endpoint; browser sessions never call /billing/upgrade or /subscriptions/change.

Return URLs and your organization’s origins

A checkout return_url (or success_url) must be an absolute HTTPS URL on an allowed origin. Core accepts the origins of the Nozle deployment and the origins you register for your own organization, for POST /checkout_sessions, Entity checkouts and Entity subscription checkout batches. Another organization’s origin is refused with 422 and return_url: [invalid_or_untrusted]. Register your origins with PUT /organizations (REST) or updateOrganization (GraphQL), which needs the developers:manage permission:
  • checkout_return_origins: up to 20 HTTPS origins, each a scheme and host only (no path, query or user info). They are stored in lowercase without a trailing slash. An invalid value answers 422 with checkout_return_origins: [value_is_invalid].
  • checkout_default_return_url: an HTTPS URL on one of the allowed origins, or null. A URL off the allowed origins answers 422 with checkout_default_return_url: [invalid_or_untrusted].
  • GraphQL: checkoutReturnOrigins and checkoutDefaultReturnUrl on updateOrganization and CurrentOrganization.
  • GET /organizations returns both settings.
When a checkout gives no return URL, the customer returns to your default return URL, else to the first registered origin, else to the payment provider’s success URL. The Engine’s POST /checkout route still checks return_url against the origins configured for the Nozle deployment before it forwards the request, and does not read your registered origins yet; see create checkout.

Response

The browser calls your authenticated merchant backend, which derives the Nozle customer and starts checkout with a restricted sk_. A Nozle publishable key is valid only for the plan catalog. If you mount embedded Stripe checkout yourself, your app also needs the Stripe publishable key for the same Stripe account connected in Nozle. Credit-funded completion response:
Scheduled transition response:
effective_at uses the customer’s timezone offset, for example 2026-11-01T00:00:00.000000+05:30. Changing an existing subscription needs a plan transition policy for that pair of plans. Without one, checkout returns 422 plan_transition_policy_required. See Create checkout for every result type and error.

React implementation

Option A — CheckoutButton (simplest)

Option B — Custom flow with useCheckoutSession

Option C — Embedded Stripe Checkout

onComplete does not fire when the session redirects to the return_url, and Nozle sessions redirect. Show success from your return page, or by reading billing status for the customer.

Server-side plan changes

For B2B scenarios where payment is already handled outside embedded checkout:
subscribe() requires a secret key (sk_) and an existing customer: an unknown customer returns 404 customer_not_found. Embedded checkout should be used when Nozle needs to collect payment before activating a paid plan.

Mixed Entity plan checkout

Seat-based products can sell different Entity plans in one invoice and one payment. Every plan in the basket must require an up-front payment: a free, trial or pay-in-arrears plan is refused with paid_plan_required. Your backend first creates or reuses active placeholder Entities, then calls the bulk Entity checkout endpoint with the selected plan for each placeholder.
Keep the secret key in your backend. The browser should send only merchant-facing intent, such as { proSeats: 2, maxSeats: 1 }; your backend derives the customer, creates stable Entity IDs, validates the return origin, and calls Nozle. Use billing_time: "anniversary" when the first successful purchase should establish the customer’s renewal day. Later Entity purchases align to that day and use Nozle’s invoice proration. See Entity Subscriptions for the complete request and response.