Skip to main content
Nozle uses Stripe to collect payments through an embedded Stripe Checkout Session, a hosted Stripe Checkout page, and automatic charges against a saved payment method. Each Nozle organization connects its own Stripe account, so your customers pay into your Stripe account, not Nozle’s. Nozle holds no global Stripe credentials.

What you need

  • A Stripe account and a secret key. A test key starts with sk_test_ and a live key with sk_live_.
  • The Stripe publishable key of the same account (pk_test_... or pk_live_...), in your browser app only. Nozle never returns it. It mounts Stripe Embedded Checkout.
  • A Nozle public API URL that Stripe can reach.
  • For React, the @stripe/stripe-js and @stripe/react-stripe-js packages. See React installation.
Nozle checks the secret key against Stripe when you connect, and rejects a key Stripe does not accept. It calls Stripe with that key to read the account, create and update customers, create Checkout Sessions and payment intents, create refunds, and create and update webhook endpoints. The key needs access to those resources. The optional customer and invoice import also lists Stripe customers and invoices.

Setup

Connect Stripe in the Nozle dashboard under Settings > Billing settings > Integrations. In the Stripe group choose Connect. When a connection already exists, the button reads Add another. The form asks for: An organization can hold several Stripe connections, but only one per Stripe account and mode. A test key (sk_test_) and a live key (sk_live_) of the same Stripe account are two connections, so you can go live after testing without disconnecting the test connection. Connecting the same account in the same mode again fails with 500 and the code stripe_account_already_connected. The message is only Internal error: it does not name the existing connection. Nozle checks the key with Stripe when you connect. A key Stripe rejects fails with 422 third_party_error and Stripe’s own message, for example Invalid API Key provided: .... An empty secret key fails with 422 validation_errors and secret_key: ["value_is_mandatory"], without calling Stripe. The dashboard shows only a generic error in these cases, so check the key first when Connect fails. The connect form has no success redirect URL. After you connect, use Add redirect URL on the connection to set one. Nozle sends the customer there after a payment on a hosted Stripe page, which covers credit top-ups and payment links. Without it the customer lands on https://stripe.com/. The URL must be valid and at most 1,024 characters. An embedded checkout uses the return_url of the checkout request instead. To edit a connection, change its name, code, Support 3DSecure and redirect URL. Changing the code updates the customers assigned to it and updates the webhook endpoint URL in Stripe. Until that update finishes, Stripe events sent to the previous code’s URL are still accepted. If the connection had no webhook endpoint, the rename registers one, and if the key may not edit webhook endpoints Nozle sends the payment_provider.error webhook. To disconnect, use Disconnect. Customers assigned to the connection lose their assignment. Nozle does not delete the webhook endpoint it created in Stripe, so delete it in Stripe yourself.

Assign the connection to customers

Checkout charges only through the connection assigned to the customer. Nozle never picks another connection for you. A customer with no assigned connection, or whose connection code matches no Stripe connection, gets 422 payment_provider_required, and nothing is created in Stripe or Nozle. To assign a connection yourself, select it in the customer’s Payment provider setting in the dashboard, which lists connections as name and code. The same section has:
  • Provider customer ID, for a customer that already exists in Stripe. It is locked once set.
  • Create customer in payment provider, which makes Nozle create the Stripe customer.
  • The payment methods the customer may use on hosted Stripe pages and automatic charges: card, link, sepa_debit, us_bank_account, bacs_debit, boleto, crypto and customer_balance. link needs card, and customer_balance cannot be combined with another method.
At checkout Nozle creates the missing Stripe customer with the customer’s name, email, address and phone, and card as the payment method when the customer has no Stripe link yet. Moving a customer to a different Stripe connection at checkout creates a new Stripe customer in that account, so saved payment methods do not carry over. To assign a connection from code, send billing_configuration.payment_provider: "stripe" and payment_provider_code when you create or update the customer. Nozle keeps the assignment even without sync_with_provider; it then creates the Stripe customer at checkout. To link a customer that already exists in Stripe from code, pass the Stripe customer ID when you upsert the customer: billingConfiguration in Node.js, billing_configuration in Python, or BillingConfiguration in Go. Nozle stores the link without creating a new Stripe customer, so payment methods already saved in Stripe stay usable. A Stripe customer can be linked to one Nozle customer only: an upsert that links a cus_… already linked to another customer answers 422 validation_errors with billing_configuration.provider_customer_id: ["value_already_exist"], and nothing is saved, so a new customer is not left half-configured. On a later upsert or update of a linked customer, Nozle sends Stripe only the name, email, phone and address that the request changed. A changed address line sends the whole address, because Stripe replaces the address as a whole. A repeated upsert, an upsert without billing_configuration, and an upsert that only links a cus_… leave the Stripe customer’s email, phone and address as they are, so edits made in Stripe are kept. Nozle never clears a Stripe customer field: a field sent as null or blank is cleared on the Nozle customer only. Credit top-ups and payment links do not assign a customer. The customer must already be assigned and linked to a Stripe customer. See Credit top-ups.

What Nozle handles

Webhook setup

Nozle registers the Stripe webhook for you. Right after you connect, a background job creates an endpoint in your Stripe account with the connection’s secret key, stores the signing secret Stripe returns, and subscribes the endpoint to the events below. The endpoint URL is built from your public Nozle API URL and has this form:
Nozle creates the endpoint with the Stripe API version it pins, 2025-04-30.basil by default. The dashboard does not show the URL or the signing secret, and has no field to enter one. Nozle subscribes the endpoint to these eight events only. Events such as charge.refunded and charge.dispute.created are not subscribed, and a valid event of any other type that reaches the endpoint is accepted and ignored. Nozle verifies the Stripe-Signature header of every request against the stored signing secret, using Stripe’s default time tolerance. A request with a bad signature, an unknown organization or code, or an unreadable body is answered with 400. A valid request is stored, queued and answered with 200. Nozle does not de-duplicate Stripe events by event ID, so a replayed delivery is processed again. Replays are safe because each handler acts on the state of the payment, invoice or upgrade: an upgrade or invoice that is already settled stays settled. Each event acts only on the records of the organization that owns the webhook endpoint. A signed event that names a payment, refund, invoice, payment request or customer of another organization is ignored. A one-time payment for an invoice of another organization, or for no known invoice, is ignored too. The same applies to the webhooks of the other payment providers. Registration is retried with increasing delays (up to 6 attempts) when Stripe rate-limits the request, is unreachable or returns a server error, so a short Stripe outage while you connect does not leave the connection without an endpoint. It still fails when the key may not create webhook endpoints, when the account has reached Stripe’s limit of 16 test-mode endpoints, or when Stripe rejects the URL, for example because it is not a public host. In each case, and when the last retry fails, Nozle sends the payment_provider.error webhook with Stripe’s message. The dashboard shows nothing, so to confirm registration open the Webhooks page of your Stripe dashboard and look for the endpoint. To recover, fix the cause and connect again. Disconnect the old connection first, or the new one fails with stripe_account_already_connected. Without the webhook, Stripe can collect the payment but Nozle is not told. Checkout status reads the Checkout Session from Stripe for a paid upgrade and reports processing once the session is complete, but it never settles a payment. Only the webhook does. Nozle never marks an invoice paid or activates a plan from a browser callback.

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 makes sure the customer exists in Stripe, creates an embedded Checkout Session and returns a stripe result.
  5. The browser mounts Stripe Embedded Checkout with the client secret and your Stripe publishable key.
  6. Stripe sends payment_intent.succeeded to Nozle.
  7. Nozle marks the invoice paid and applies the change: it activates the new subscription, or applies the upgrade.
  8. Your app confirms the result. For a new subscription, reload the subscription. For an upgrade, poll checkout status.
If prepaid credits fully cover the invoice, Nozle returns a completed checkout and Stripe is not called. The return_url you send must be an allowlisted HTTPS origin, and Nozle passes it to Stripe as the return URL of the session. For an invoice that already exists, call checkout with the invoice instead of a plan. It returns the same stripe result. The invoice must not be paid, void or a draft. An invoice has at most one payable Checkout Session at a time. This applies to invoice checkout and to resumed or Entity subscription checkout:
  • Calling checkout again for the same invoice, with or without an Idempotency-Key, returns the session that is still open, with the same client_secret.
  • A session that expires within 10 minutes, or that was opened for another amount, is expired at Stripe before exactly one new session is created.
  • While the customer’s payment of the last session is processing or has succeeded, checkout answers 422 validation_errors with base: [payment_already_processing] instead of opening a session that could charge the customer twice.
  • If a second payment still succeeds for an invoice that is already paid, Nozle does not apply it. It refunds the payment in full at Stripe with refund reason duplicate, and sends you the invoice.payment_failure webhook with provider_error.error_code duplicate_payment_refunded and the payment intent in error_details.provider_payment_id. If Stripe refuses the refund, the code is duplicate_payment_refund_failed: refund it in the Stripe dashboard. A payment that was still processing when the invoice was paid another way, for example marked as paid, is not refunded automatically; the code is duplicate_payment_requires_review.

The checkout result

  • client_secret is the cs_ secret of an embedded Checkout Session. clientSecret is the same value.
  • A plan or invoice checkout result has no Stripe-hosted URL, no Stripe publishable key, no checkout_id and no expires_at. Only credit top-ups return a Stripe-hosted payment_url. See Credit top-ups.
  • amount_cents is in the smallest unit of the invoice currency.
  • A paid upgrade returns plan_upgrade_intent_id instead of invoice_id. It is the ID you poll. The session stays open for 35 minutes.
  • The Checkout Session of a new-subscription or single Entity checkout expires after 23 hours, when the unpaid checkout itself expires.
  • An Entity checkout adds external_entity_id and external_subscription_id.
  • Send an Idempotency-Key. A retry of an invoice checkout returns the session that is still open for that invoice.
  • Starting the same upgrade again returns the session that is still open. Starting a different plan expires the open session and replaces it. If the open session cannot be replaced, for example while a payment is processing, Nozle answers 409 plan_upgrade_checkout_in_progress.
  • 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 session.
  • When the upgrade’s time runs out, Nozle asks Stripe before it reports the upgrade expired: an open session is expired at Stripe, a completed session with a succeeded payment is settled and applied as its webhook would be, and a payment still processing (or a Stripe error) answers 409 plan_upgrade_checkout_in_progress. 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 session expired before its time, for example because Stripe expired it, the upgrade itself stays open. A new upgrade checkout then returns 409 plan_upgrade_checkout_in_progress until the upgrade’s own 35 minutes pass, even though status already reports expired.
  • A retry of a confirmed quoted upgrade can return {"type": "processing", "checkout_id", "status"}, where checkout_id is the upgrade ID. This is the answer while Nozle waits for the webhook of a payment that is already complete. Recover it with checkout status rather than starting another checkout.

Retry a new-subscription checkout

A new-subscription checkout leaves an incomplete subscription behind until the payment succeeds. 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 React CheckoutButton retry does this. 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 Stripe 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 Checkout Session is expired. See Unpaid checkout expiry. An invoice checkout forwards its Idempotency-Key to Stripe. Reusing a key with a different body returns 409 idempotency_key_reused.

Create and poll

Checkout and status calls need an sk_ key, so they run only in your backend. Bind each upgrade ID to the buyer who started it before you call Nozle. Checkout from an invoice sends no return_url through the SDKs, so Nozle uses its configured default return URL. A new-subscription or invoice checkout has no checkout ID to poll. After Embedded Checkout completes, reload the subscription. For example, call GET /billing/status with the customer until it returns the plan the customer bought. It returns 404 while the subscription is not active.

Mount the session in the browser

Mount the cs_ secret with Stripe Embedded Checkout and the publishable key of the Stripe account connected in Nozle. A Nozle pk_nozle_ key cannot do this: it is valid only for the plan catalog.

React

With the React SDK, CheckoutButton calls your createCheckout callback and hands the Stripe client secret to onStripeClientSecret. Checkout mounts the secret. It uses Embedded Checkout for a cs_ secret:
CheckoutButton throws onStripeClientSecret is required for embedded Stripe checkout when the callback is missing. The onComplete of the embedded Checkout does not fire when the session redirects to the return_url, and Nozle creates sessions that redirect. Show success from your return page instead, or by reading billing status for the customer. Neither the return page nor onComplete proves activation: reload the subscription or poll status before you show success. See the React components, the hooks and the billing portal, which recovers an open upgrade after a reload.

HTTP

The SDKs call these Engine endpoints. Create and status need a secret key with payment:write or payment:read: Both status scope parameters are required together. Core, called directly at GET /api/v1/checkout_sessions/{id}, names them customer_id and external_subscription_id. Without them the call returns 404 checkout_session_not_found, and with only one it returns 400. The verify endpoint is for Razorpay orders only. Nozle has no verify step for Stripe.

How Nozle settles a payment

  • Webhook only. Nozle settles a Stripe payment only on payment_intent.succeeded. It finds the payment from the data it attached when it created the session. A payment that Nozle did not create is ignored.
  • Upgrade checks. Before it applies an upgrade, Nozle checks that the upgrade is still payable, that the payment is the one it expects, that the customer still uses the same Stripe connection, and that the subscription is still active with no pending change. It then recomputes the upgrade invoice and compares it with the quoted amount and currency. Nozle does not compare the amount Stripe collected with the quote. Any mismatch moves the upgrade to needs_review.
  • Statuses. Nozle maps the Stripe payment status like this:
  • Declined cards. On a payment-gated checkout a declined card keeps the invoice open, so the customer can try again in the same session. Any other failure marks the payment failed, closes the invoice and cancels the incomplete subscription.
A browser success callback alone never activates a plan.

Status

GET /checkout/{plan_upgrade_intent_id} returns the status of a paid upgrade: While the upgrade awaits payment, each status call retrieves its Checkout Session from Stripe. It reports processing when Stripe says the session is complete and expired when the session is not open any more. A status call never settles a payment: succeeded appears only after the webhook has applied the upgrade. fulfillment_status is pending until the upgrade is applied, then succeeded. Show success only when both are succeeded, then reload the subscription. The response also carries checkout_id (the upgrade ID), provider (stripe), amount_cents, currency and invoice_id. invoice_id is empty until the upgrade is applied. While the status is awaiting_payment, a nested checkout carries the client secret of the still-open session, so a client can resume after a reload. needs_review means Stripe took a payment that Nozle could not apply. It also appears when Stripe could not confirm whether it created the session. Two causes are easy to hit today. First, a billing entity with an invoice grace period above 0 days leaves the upgrade invoice as a draft, so the upgrade ends in needs_review with plan_upgrade_amount_changed when the policy credits the unused time, and otherwise stays pending because the payment handler fails with payment_status_update_on_draft_invoice. Second, an upgrade checkout created while a downgrade is pending ends in needs_review with plan_upgrade_subscription_changed. Set the grace period to 0 and withdraw a pending downgrade before you start the upgrade. See Paid upgrades: current limits. Nozle grants no plan and records no successful payment for it. Check the payment in the Stripe dashboard and refund or reconcile it deliberately. Nozle uses a hosted Stripe Checkout page where the customer is not inside your embedded checkout: an invoice payment URL, a credit top-up purchase, and the link for overdue invoices. The page offers the payment methods set on the customer. An invoice payment link expires after 24 hours. The result is a payment_url, which you navigate the customer to. After payment the customer goes to the connection’s success redirect URL. payment_intent.succeeded settles it.

Credit top-ups

A credit top-up for a Stripe customer returns a hosted Stripe Checkout page as payment_url, and payment_provider is stripe. Navigate to it. Credits are granted once the top-up invoice is fully paid and the credit signal is delivered. A Stripe payment pays it; so does a manual payment for the full amount, a payment request, or marking the invoice paid.
The customer must already be assigned to the Stripe connection and linked to a Stripe customer. If not, the call fails with 422 validation_errors and no_linked_payment_provider or missing_payment_provider_customer. If the customer has a currency, it must match the package currency, or the call fails with validation_errors and customer_currency_mismatch. See Paid Credit Top-ups and Create Credit Top-up Purchase.

Saved payment methods and automatic charging

  • Saving a card. Checkout asks Stripe to save the payment method for later off-session use. Nozle skips this for customers whose country is IN and for customers who use crypto.
  • Automatic charges. When an invoice is issued for a customer who has a Stripe customer ID, Nozle charges it through Stripe on its own, using the customer’s saved payment method. This covers renewals. Checkout and top-up invoices are paid through their own session instead. If the saved method is gone, Nozle uses the first payment method it finds on the Stripe customer. Customers in India and customers who pay by customer_balance bank transfer are charged on session, not off session.
  • Authentication. Stripe can refuse an off-session charge with authentication_required. With Support 3DSecure on, Nozle retries it on session. The payment moves to requires_action, Nozle sends the payment.requires_action webhook, and the invoice stays pending until the customer completes the action. With it off, the payment fails.
  • Small amounts. An amount_too_small error leaves the invoice pending instead of failed.
  • Keeping methods current. setup_intent.succeeded makes the new method the customer’s default. customer.updated follows a default change made in Stripe. payment_method.detached clears the stored default when the customer removes that card in Stripe.
  • Merchant webhooks. Nozle sends payment.succeeded when a payment succeeds, and payment.requires_action when a payment needs the customer.

Refunds and disputes

  • Refunds. A credit-note refund is sent to Stripe against the invoice’s latest successful payment. Nozle maps the credit-note reason to the Stripe reason and records the status Stripe returns. A card refund normally comes back succeeded at once, and the credit note is then refunded. The refund stays pending only when Stripe reports it as pending, until charge.refund.updated delivers the final status. Once the credit note has succeeded, a later charge.refund.updated is ignored. If Stripe rejects the refund or reports it failed, Nozle marks the refund failed and sends the credit_note.refund_failure webhook. Nozle creates no refund for an invoice whose dispute was lost.
  • Disputes. On charge.dispute.closed with status lost, Nozle marks the invoice’s dispute lost and sends the invoice.payment_dispute_lost webhook. A dispute that closes any other way changes nothing.
  • Top-ups. A credit note or a lost dispute on a paid credit top-up invoice also takes back the top-up’s unspent credits. See Refunds and lost disputes.

Import existing Stripe customers and invoices

Use Sync Stripe customers and invoices on the connection to bring an existing Stripe account into Nozle. A preview comes first and changes nothing. Nozle then creates missing customers, links matching ones and imports invoices as read-only history. It changes no Stripe data and no subscription. The preview and the sync run in the background. Starting one returns a sync run at once, and its status moves through queued, previewing, previewed, syncing_customers, syncing_invoices, then completed or failed. The customer sync imports the invoices after the customers without a second call. counts gives live totals per object type and status while the run works. A record that conflicts with another, or is invalid, is marked conflict or failed and the run goes on. From code, the same steps are POST /api/v1/integrations/stripe/sync/preview (payment_provider_id, optional mappings of stripe_customer_id to external_customer_id), POST /api/v1/integrations/stripe/sync/customers with the previewed run’s run_id, and GET /api/v1/integrations/stripe/sync-runs/{id} to poll, on the Core API with a secret key (with API key permissions: customer:write, and customer:read to poll). Each answers {"stripe_sync_run": {...}}. The dashboard polls the GraphQL query stripeSyncRun the same way. Poll until the run is completed or failed. Starting a run that is still working answers 409 stripe_sync_in_progress, and starting a run that already finished answers 422 stripe_sync_run_finished: start a new sync from a new preview. Nozle matches a Stripe customer to an existing link, then to the mapping you set in the preview, then to its metadata, and otherwise creates a customer. The metadata keys are nozle_external_customer_id, external_customer_id, org_id, organization_id and workspace_id. A customer whose email matches one Nozle customer is a suggestion to confirm, not an automatic link. The sync reads at most 10,000 customers and 10,000 invoices, and the preview shows the first 500 rows. A linked customer can then pay by top-up and automatic charge.

Errors

Checkout errors that are specific to Stripe. See Create checkout for the full list. Connecting Stripe can fail with 500 stripe_account_already_connected (message Internal error), or with 422 third_party_error and Stripe’s message when Stripe rejects the secret key or no key is given.

Limits

  • Plan and invoice checkout use embedded Checkout only. Nozle returns no Stripe-hosted URL for them. A hosted Stripe page (checkout.stripe.com, valid for 24 hours) is used for credit top-ups, invoice payment URLs and overdue invoices.
  • A new-subscription or invoice checkout has no checkout ID and no status call. A paid upgrade has both.
  • An upgrade session lasts 35 minutes. A new-subscription, single Entity or bulk Entity checkout lasts 23 hours. See Entity Subscriptions.
  • One connection per Stripe account and mode (test or live) in an organization. The secret key of a connection cannot change.
  • Nozle manages the webhook signing secret. You cannot enter it in the dashboard.
  • Bank transfers through customer_balance support USD, GBP, JPY, MXN and EUR. For EUR the customer or your billing entity must be in Belgium, Germany, Spain, France, Ireland or the Netherlands.
  • Nozle does not restrict the payment methods of an embedded Checkout Session. Which methods appear depends on your Stripe settings.

Troubleshooting

Test your flows with a test-key connection before you connect live keys.