> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nozle.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing and subscriptions

> List plans, create payment-aware checkouts, and manage customer or Entity subscriptions from a trusted backend.

## List plans and create checkout

`Plans` accepts `pk_` or `sk_`; other billing methods require a server-side `sk_`. `Plan` includes code, name, amount in cents, currency, and interval; the Go struct drops every other field of the API response. The list itself includes trial, pay-in-arrears and pending-deletion plans, and with a `pk_` key the full plans are readable by anyone who has the key. See the [public plan catalog](/api/list-plans). Upsert the customer with `UpsertCustomer` before billing.

```go theme={null}
import nozle "github.com/nozle-dev/nozle-go"

client := nozle.New("") // reads NOZLE_SECRET_KEY

plans, err := client.Plans(ctx)
checkout, err := client.Checkout(ctx, nozle.CheckoutParams{
  CustomerID: "workspace_123", PlanCode: "pro_monthly",
  ReturnURL: "https://app.example.com/billing",
  IdempotencyKey: "checkout-workspace-123-pro-v1",
})
_ = plans
_ = checkout
_ = err
```

Validate the exact HTTPS return origin before using a browser-supplied URL. Browser redirects and callbacks do not prove activation. Handle each `CheckoutResult.Type` explicitly:

| `Type` | API fields | Handling |
| - | - | - |
| `stripe` | `client_secret` (`cs_...`), `invoice_id`, `amount_cents`, `currency` | Mount Stripe embedded Checkout with `ClientSecret`. Plan and invoice checkout never return a Stripe URL; only credit top-ups return a Stripe-hosted `payment_url`. See [the Stripe flow](/guides/billing/checkout#stripe). |
| `razorpay` | `checkout_id`, `key_id`, `order_id`, `amount_cents`, `currency`, `expires_at`, `invoice_id`; with a mandate also `customer_id`, `recurring: true`, `mandate_max_amount_cents` | Open Razorpay Checkout, then verify through your backend. See [the Razorpay flow](/guides/billing/checkout#razorpay). |
| `hosted` | `payment_url` | Not returned by `Checkout` or `CheckoutInvoice`. The type still exists for a Razorpay payment link: navigate to `PaymentURL`. Invoice payment URLs and credit top-ups return a `payment_url` without a `type`. |
| `processing` | `checkout_id`, `status` | Razorpay: the order is not ready or has expired. Stripe: a replayed quoted upgrade whose webhook is still pending. Recover with `CheckoutStatus`. `Status` is an internal operation status. |
| `completed` | `status: "succeeded"`, `subscription_id`, `plan_code`, `invoice_id` (may be `null`), `amount_cents`, `currency`, `payment_source` (`credits` or `none`) | Reload the subscription. |
| `scheduled` | `status: "pending"`, `subscription_id`, `plan_code`, `transition_direction`, `transition_policy`, `effective_at` | Display the pending change and its effective date. |

A paid upgrade returns `plan_upgrade_intent_id` instead of `invoice_id` on `stripe` and `razorpay` results. On `scheduled`, `SubscriptionID` is the pending subscription. In `v0.9.1`, `URL`, `PublishableKey`, `StripeAccount` and `PendingSubscriptionID` were removed from `CheckoutResult` because checkout never returned them; code that reads them no longer compiles. `CheckoutResult` decodes `expires_at`, `customer_id`, `recurring`, `mandate_max_amount_cents`, `plan_upgrade_intent_id`, `plan_code`, `payment_source`, `transition_direction`, `transition_policy`, `external_entity_id` and `external_subscription_id`.

Checkout errors: `400 invalid_return_url` when the `return_url` origin is not allowed (checked before anything else); `404 plan_not_found` or `customer_not_found`; `409 subscription_selection_required` when the customer has several active subscriptions and no `SubscriptionID` was given; `422 checkout_not_required` when the plan needs no up-front payment (free, trial, or pay-in-arrears) and the customer has no subscription, so use `Subscribe` instead; `422 payment_provider_required` when no payment provider is connected; `422 plan_already_active` when the customer is already on that plan; `409 subscription_checkout_in_progress` when a payment for the customer's new-subscription checkout on a different plan is still processing, or it was paid (an abandoned checkout is replaced instead, and the same plan reopens it).

## Manage a payment method

```go theme={null}
page, err := client.ManagePaymentMethod(ctx, "workspace_123", "https://app.example.com/settings/billing", "") // last argument: optional idempotency key
if err != nil {
	return err
}
http.Redirect(w, r, page.URL, http.StatusSeeOther) // Stripe Billing Portal
```

Stripe only. Requires an `sk_` key. A `pk_` key is refused with `403` (no error code, only a message). Nozle stores no card data: the customer changes their payment method in the Stripe Billing Portal. Razorpay customers pay each invoice through a Razorpay payment link, so for them the call fails with `422 razorpay_pays_by_link`.

See [Manage Payment Method](/api/manage-payment-method) for the HTTP contract and error codes.

## Direct subscription and cancellation

`Subscribe` and `CancelSubscription` expose the returned nested `Subscription` and fill the convenience `SubscriptionID` and `Status` fields.

```go theme={null}
_, err := client.UpsertCustomer(ctx, nozle.Customer{ExternalID: "workspace_123", Name: "Acme"})
if err != nil { return err }
subscription, err := client.Subscribe(ctx, nozle.SubscribeParams{CustomerID: "workspace_123", PlanCode: "free"})
if err != nil { return err }
cancelled, err := client.CancelSubscription(ctx, "workspace_123", subscription.SubscriptionID, "end_of_period")
_ = cancelled
_ = err
```

The customer must already exist, so call `UpsertCustomer` first; an unknown customer returns `404 customer_not_found` and nothing is created. Calling `Subscribe` again for the same customer and plan returns the same subscription. Use direct subscription only when payment is not required: `Subscribe` accepts a paid plan without collecting payment. Cancellation accepts `end_of_period` or `immediate`.

`Subscribe` to another plan adds a second active subscription. It does not replace the first, and plan credit grants are granted again. To move a customer, call `Checkout` with `SubscriptionID` (needs a [transition policy](/api/plan-transition-policies)), or cancel and then subscribe. Pass the new subscription ID as `SubscriptionID` to `Track`.

## Subscription self-service

Derive the customer from the authenticated application user and validate ownership of the selected **external** subscription ID. Do not pass Core's internal UUID. These methods support [the React billing portal](/sdks/react/billing-portal).

Changing an existing subscription's plan needs a [transition policy](/api/plan-transition-policies) for that source and target plan pair. Without one, preview, checkout, and transitions return `422 plan_transition_policy_required`. Exception: from a \$0 plan to a paid plan, the organization's free-to-paid default applies without a pair policy.

### Load options and preview a quote

`CheckoutResult.Type` distinguishes `stripe`, `razorpay`, `hosted`, `processing`, `completed`, and `scheduled`. Payment fulfillment is authoritative on the server. Use `CheckoutStatus` and `VerifyCheckout` with a `CheckoutScope` containing both customer and external subscription IDs when recovering payment.

For a signed plan-change quote, call `SubscriptionOptions`, then `PreviewSubscriptionChange`. Pass its `QuoteID` and the external `SubscriptionID` to `Checkout`. A quote can expire; ask the customer to confirm a fresh one after a 409.

```go theme={null}
options, err := client.SubscriptionOptions(ctx, "workspace_123", "subscription_123")
quote, err := client.PreviewSubscriptionChange(ctx, "workspace_123", "subscription_123", "growth_monthly")
_ = options
_ = quote
_ = err
```

`SubscriptionOptions` contains the current subscription, pending change, eligible plans, and checkout as JSON values. `SubscriptionChangePreview` contains the signed quote, direction, timing, `EffectiveAt`, `RenewalAt`, and exact `json.Number` amounts including `AmountDueNowCents` and `AmountDueAtEffectiveCents`. The API also returns `transition_policy`, `billing_anchor`, `proration_behavior`, and `credit_action`, which the type omits. Show server-provided eligibility, amounts, and dates before confirmation.

### Confirm a paid upgrade

```go theme={null}
quote, err := client.PreviewSubscriptionChange(ctx, "workspace_123", "subscription_123", "growth_monthly")
if err != nil { return err }
checkout, err := client.Checkout(ctx, nozle.CheckoutParams{
  CustomerID: "workspace_123", SubscriptionID: "subscription_123",
  PlanCode: "growth_monthly", QuoteID: quote.QuoteID,
  ReturnURL: "https://app.example.com/settings/billing",
  IdempotencyKey: "upgrade-subscription-123-growth-v1",
})
_ = checkout
_ = err
```

Quotes expire after five minutes or when their bound state changes. On HTTP 409, request a fresh preview and confirmation. Retry an uncertain accepted request with the same payload and key.

A \$0 to \$0 immediate upgrade returns `completed` with `payment_source: "credits"` and creates a \$0 invoice. An upgrade checkout to a pay-in-arrears plan returns `422 plan_upgrade_invoice_missing`.

### Schedule or withdraw a downgrade

`PreviewSubscriptionTransition` and `ApplySubscriptionTransition` support `cancel`, `downgrade`, and `uncancel`. Apply requires a caller-supplied idempotency key. `WithdrawPendingSubscriptionChange` requires the exact pending change ID. `CancelSubscription` provides the simpler immediate or end-of-period cancellation path.

```go theme={null}
quote, err := client.PreviewSubscriptionChange(ctx, "workspace_123", "subscription_123", "growth_monthly")
if err != nil { return err }
scheduled, err := client.ApplySubscriptionTransition(ctx, nozle.SubscriptionTransitionParams{
  CustomerID: "workspace_123", SubscriptionID: "subscription_123",
  Operation: "downgrade", TargetPlanCode: "growth_monthly",
  Timing: "end_of_period", BillingAnchor: "keep_anchor", QuoteID: quote.QuoteID,
}, "downgrade-subscription-123-growth-v1")
_ = scheduled
_ = err
withdrawn, err := client.WithdrawPendingSubscriptionChange(ctx,
  "workspace_123", "subscription_123", "confirmed-pending-change-id", "withdraw-subscription-123-v1")
_ = withdrawn
_ = err
```

Reload options after scheduling. The withdraw result is `{subscription: {id, external_id, plan_code, status}, withdrawn_pending_subscription_id, replayed}`; the same key returns the same body with `replayed: true`.

A new scheduled change while a change is pending replaces it with a new pending ID. A paid upgrade checkout is refused with `409 subscription_change_pending` while a downgrade or cancellation is scheduled: withdraw the downgrade, or keep the subscription, first. Scheduling a change or cancelling at period end cancels an open upgrade checkout. Withdrawing the replaced ID returns `409 pending_subscription_changed`. An unknown ID returns `404 pending_subscription_not_found`.

`PreviewSubscriptionTransition` and `ApplySubscriptionTransition` use the `Timing` you pass even when the pair's transition policy says otherwise; `Checkout` always follows the policy. A `downgrade` on a pair whose policy direction is `upgrade` returns `422 target_plan_is_not_a_downgrade`.

### Cancel at period end and Keep

```go theme={null}
params := nozle.SubscriptionTransitionParams{
  CustomerID: "workspace_123", SubscriptionID: "subscription_123",
  Operation: "cancel", Timing: "end_of_period",
}
preview, err := client.PreviewSubscriptionTransition(ctx, params)
if err != nil { return err }
params.ExpectedEffectiveAt = preview.SubscriptionTransition.EffectiveAt
_, err = client.ApplySubscriptionTransition(ctx, params, "cancel-subscription-123-v1")
if err != nil { return err }
_, err = client.ApplySubscriptionTransition(ctx, nozle.SubscriptionTransitionParams{
  CustomerID: "workspace_123", SubscriptionID: "subscription_123", Operation: "uncancel",
}, "keep-subscription-123-v1")
_ = err
```

Preserve the preview's full timestamp. Keep is available only before the ending is due. Cancellation clears a pending downgrade; Keep does not restore it.

### Scope payment recovery and verification

```go theme={null}
scope := nozle.CheckoutScope{CustomerID: "workspace_123", SubscriptionID: "subscription_123"}
status, err := client.CheckoutStatus(ctx, "checkout_123", scope)
_ = status
_ = err
verified, err := client.VerifyCheckout(ctx, "checkout_123", nozle.RazorpayVerification{
  RazorpayOrderID: "order_from_callback", RazorpayPaymentID: "payment_from_callback",
  RazorpaySignature: "signature_from_callback",
}, scope)
_ = verified
_ = err
```

Forward the genuine Razorpay callback. Scope recovery to both customer and external subscription IDs. Reload the subscription before reporting activation.

## Entity subscriptions

Entity subscription methods include `EnsureEntitySubscription`, `GetEntitySubscription`, `ListEntitySubscriptions`, `CheckoutEntitySubscription`, `ChangeEntitySubscriptionPlan`, `CancelEntitySubscription`, and `RemoveEntitySubscription`. `CancelEntitySubscription` returns `EntitySubscriptionCancelResult` with the Entity subscription and transition settlement details.

```go theme={null}
_, err := client.EnsureEntitySubscription(ctx, "workspace_123", "user_42")
if err != nil { return err }
seatCheckout, err := client.CheckoutEntitySubscription(ctx, "workspace_123", "user_42", nozle.EntitySubscriptionChange{
  PlanCode: "pro_monthly", BillingTime: "anniversary",
  ReturnURL: "https://app.example.com/settings/billing", IdempotencyKey: "checkout-user-42-pro-v1",
})
if err != nil { return err }
_ = seatCheckout
cancelled, err := client.CancelEntitySubscription(ctx, "workspace_123", "user_42", nozle.EntitySubscriptionCancel{
  Timing: "end_of_period", CreditAction: "none", IdempotencyKey: "cancel-user-42-v1",
})
_ = cancelled
_ = err
```

`BillingTime` accepts only `calendar` or `anniversary`. Single Entity checkout also accepts free, trial, and pay-in-arrears plans; it returns `completed` with `payment_source: "none"`. `ChangeEntitySubscriptionPlan` needs a [transition policy](/api/plan-transition-policies) for the plan pair and does not send `BillingTime`. It returns `scheduled` or `completed`, plus `ExternalEntityID` and `ExternalSubscriptionID`. `CancelEntitySubscription` on a pay-in-arrears plan without `CreditAction` returns `422` with `credit_action: not_supported_for_pay_in_arrears`. `RemoveEntitySubscription` on an Entity with billing history returns `403 entity_subscription_has_billing_history`.

### Buy several Entity plans together

`CheckoutEntitySubscriptions` buys plans for several Entities with one payment. Do not loop over `CheckoutEntitySubscription`: that creates separate invoices and payments.

```go theme={null}
batch, err := client.CheckoutEntitySubscriptions(ctx, "workspace_123", []nozle.EntitySubscriptionCheckoutItem{
  {ExternalEntityID: "user_42", PlanCode: "pro_monthly"},
  {ExternalEntityID: "user_43", PlanCode: "max_monthly"},
}, "https://app.example.com/settings/billing", "anniversary", "seats-workspace-123-v1")
_ = batch
_ = err
```

The arguments after the items are the return URL, billing time, and idempotency key. `EntitySubscriptionCheckoutManyResult` contains `ID`, `Type` (`stripe`, `razorpay`, `processing`, or `completed`), `Status` (`pending`, `open`, `completed`, `failed`, or `expired`), `ClientSecret`, `InvoiceID`, `AmountCents`, `Currency`, `Replayed`, `ExpiresAt`, and `Items` with each Entity's external IDs, plan code, and subscription status. Razorpay results add `CheckoutID`, `KeyID`, and `OrderID`.

Send 1 to 100 unique Entities. The idempotency key is required, up to 255 bytes, and sent as the `Idempotency-Key` header. The same key and request returns `Replayed: true` and reuses the open session; replaying a `failed` or `expired` batch returns `checkout_not_available`. The checkout expires after 23 hours.

Every plan must require up-front payment: pay in advance with an amount above 0 and no trial, or pay-in-advance fixed charges. Otherwise the whole batch fails with `422 validation_errors` and `error_details.items: ["paid_plan_required"]`. Other item errors are `mixed_currencies` and `mixed_intervals`. An untrusted return URL returns `422` with `return_url: invalid_or_untrusted`. With no payment provider connected, the call returns `422 payment_provider_required`, as single checkout does.

See [Entity subscriptions](/api/entity-subscriptions) for lifecycle and payment behavior, and [bulk Entity checkout](/api/entity-subscriptions#purchase-multiple-entity-plans-together).

## Settlement transitions

For configured settlement policies, preview and apply with `SubscriptionTransitionParams`. Apply keys are limited to 255 UTF-8 bytes. See [Subscriptions](/guides/billing/subscriptions) for valid combinations.

## Read usage

From `v0.12.0`.

```go theme={null}
current, err := client.CurrentUsage(ctx, "cus_123", "")      // this billing period
past, err := client.PastUsage(ctx, "cus_123", "", 6)          // newest first
```

Each period is a `CustomerUsage` with `FromDatetime`, `ToDatetime`, `Currency`, `AmountCents`, `TotalAmountCents` and `ChargesUsage` (units and amount per charge and feature). Pass a subscription ID instead of `""` to read a specific subscription.

Without a subscription ID, the customer's one active subscription is used, and a customer with none or several gets an error. The client remembers that subscription, as `Track` does, and shares it between `Track`, `CurrentUsage` and `PastUsage`. `Checkout`, `CheckoutInvoice`, `CheckoutStatus`, `VerifyCheckout`, `Subscribe`, `CancelSubscription` and `ApplySubscriptionTransition` forget it. A change made elsewhere, such as in the dashboard, is not seen, so pass the subscription ID when that matters.

## Invoices

From `v0.12.0`.

```go theme={null}
page, err := client.ListInvoices(ctx, "cus_123", nozle.ListInvoicesParams{Status: "finalized", Page: 1})
invoice, err := client.GetInvoice(ctx, page.Invoices[0].NozleID)
pdf, err := client.InvoiceDownloadURL(ctx, invoice.NozleID)    // "" until the PDF exists
```

`ListInvoicesParams` filters the list, newest first. Empty fields are not sent:

| Field | Description |
| - | - |
| `Status` | `draft`, `finalized`, `voided`, `failed` or `pending`. |
| `PaymentStatus` | `pending`, `succeeded` or `failed`. |
| `PaymentOverdue` | `*bool`. `true` keeps only unpaid invoices past their due date; `false` keeps the others. |
| `IssuingDateFrom`, `IssuingDateTo` | Inclusive issuing dates, `YYYY-MM-DD`. |
| `Page`, `PerPage` | Page number and page size. |

```go theme={null}
overdue := true
unpaid, err := client.ListInvoices(ctx, "cus_123", nozle.ListInvoicesParams{PaymentOverdue: &overdue})
```

Pay an open invoice with `CheckoutInvoice`.

A PDF is generated the first time it is asked for, so the download call returns no URL until it exists; ask again a few seconds later, or wait for the `invoice.generated` webhook.

## Alerts

From `v0.12.0`.

```go theme={null}
target := nozle.AlertTarget{SubscriptionID: "sub_123"}
_, err := client.CreateAlert(ctx, target, nozle.AlertInput{
	AlertType:   "feature_current_usage_units",
	Code:        "api_calls_80",
	FeatureCode: "api_calls",
	Thresholds:  []nozle.AlertThreshold{{Value: "40000"}, {Value: "10000", Recurring: true}},
})
alerts, err := client.ListAlerts(ctx, target, 0)
_, err = client.UpdateAlert(ctx, target, "api_calls_80", nozle.AlertInput{Name: "API calls at 80%"})
_, err = client.DeleteAlert(ctx, target, "api_calls_80")

// Balance alerts belong to a wallet:
wallet := nozle.AlertTarget{CustomerID: "cus_123", WalletCode: "main"}
_, err = client.CreateAlert(ctx, wallet, nozle.AlertInput{
	AlertType:  "wallet_credits_balance",
	Code:       "low_credits",
	Thresholds: []nozle.AlertThreshold{{Value: "100"}},
})
```

Usage alert types are `current_usage_amount`, `feature_current_usage_amount` and `feature_current_usage_units` (the `feature_*` ones need a feature code). Wallet alert types are `wallet_balance_amount`, `wallet_credits_balance`, `wallet_ongoing_balance_amount` and `wallet_credits_ongoing_balance`. Threshold values are cents, units or credits by type, and at most one threshold is recurring. Nozle sends the `alert.triggered` webhook when a threshold is crossed; [verify it](/api/receive-webhooks) before acting.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.