Entity Subscriptions
curl --request POST \
--url https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout")
.header("Authorization", "Bearer <token>")
.asString();Checkout & Subscriptions
Entity Subscriptions
Give individual customer Entities independent plans, billing periods, and subscription lifecycles.
POST
/
customers
/
{customer_id}
/
entity-subscriptions
/
checkout
Entity Subscriptions
curl --request POST \
--url https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout"
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, headers=headers)
print(response.text)const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout"
req, _ := http.NewRequest("POST", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}require 'uri'
require 'net/http'
url = URI("https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}HttpResponse<String> response = Unirest.post("https://api.nozle.app/api/v1/customers/{customer_id}/entity-subscriptions/checkout")
.header("Authorization", "Bearer <token>")
.asString();Entity subscriptions let one customer pay for multiple independently managed subscriptions. Each Entity keeps a stable subscription identity while using the standard Nozle plan, invoice, checkout, renewal, and reporting lifecycle.
Auth: secret key only. Publishable keys are rejected.
Nozle validates the live Entity before creating or changing its subscription. Suspended or deleted Entities cannot start checkout. A newly created Entity that reuses an older external ID cannot inherit the older Entity’s subscription.
This operation is idempotent. It creates the stable Entity-to-subscription identity but does not select or activate a plan.
List all Entity subscriptions for a customer:
The response reports the current and pending plans independently for each Entity:
The Python SDK exposes the same backend operation:
On the frontend, pass a Stripe result to the React checkout component:
The endpoint below remains available for backends that do not use the Node SDK.
The request accepts up to 100 unique Entities. Every plan must be paid, use the same currency, and use the same interval. Each Entity must be active and must not already have an active, pending, or incomplete subscription.
Nozle supports these result types:
The configured plan-transition policy determines whether the change is immediate, payment-gated, or scheduled for the renewal boundary. Changing one Entity never changes another Entity’s subscription.
Cancellation supports the same settlement controls as customer subscriptions. Prefer
Removal is allowed only when the identity has no active, pending, or incomplete subscription. Historical billing records remain immutable.
Customer: workspace_123
├── Entity: user_42 → Pro monthly
└── Entity: user_43 → Max annual
Ensure a subscription identity
PUT /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
Read subscriptions
Read one Entity subscription:GET /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
GET /customers/workspace_123/entity-subscriptions
Authorization: Bearer sk_...
{
"external_customer_id": "workspace_123",
"external_entity_id": "user_42",
"external_subscription_id": "entity-sub-7e1782f4-...",
"status": "active",
"current_plan": {
"code": "pro_monthly",
"name": "Pro",
"interval": "monthly",
"amount_cents": 1499,
"amount_currency": "USD",
"status": "active"
},
"pending_plan": null,
"billing_time": "anniversary",
"cancel_at_period_end": false
}
Start checkout
POST /customers/workspace_123/entities/user_42/subscription/checkout
Authorization: Bearer sk_...
Idempotency-Key: checkout-user-42-pro-v1
Content-Type: application/json
{
"plan_code": "pro_monthly",
"billing_time": "anniversary",
"return_url": "https://app.example.com/settings/billing"
}
billing_time accepts anniversary or calendar for the initial subscription. Paid plans remain incomplete until verified payment succeeds. Retrying an incomplete checkout for the same plan reuses its subscription and invoice instead of creating another charge.
Purchase multiple Entity plans together
Use bulk Entity checkout when a customer purchases a mixed pool of plans in one payment. The merchant backend supplies one item per Entity; Nozle creates one invoice and one Stripe checkout for the complete basket. Use the Node SDK from your trusted backend. It wraps the API request, validates the basket, and returns the checkout contract:import { Nozle } from '@nozle-js/node'
const nozle = new Nozle({
apiKey: process.env.NOZLE_SECRET_KEY!,
eventsUrl: 'https://core.nozle.app',
})
const checkout = await nozle.entitySubscriptions.checkoutMany('workspace_123', {
billingTime: 'anniversary',
returnUrl: 'https://app.example.com/settings/billing',
idempotencyKey: 'workspace-123-seat-purchase-v1',
items: [
{ externalEntityId: 'seat_pro_001', planCode: 'pro_monthly' },
{ externalEntityId: 'seat_max_001', planCode: 'max_monthly' },
],
})
// Return this result to your frontend. Never expose NOZLE_SECRET_KEY.
import os
from nozle import Nozle
nozle = Nozle(
api_key=os.environ["NOZLE_SECRET_KEY"],
events_url="https://core.nozle.app",
)
checkout = nozle.entity_subscriptions.checkout_many(
"workspace_123",
billing_time="anniversary",
return_url="https://app.example.com/settings/billing",
idempotency_key="workspace-123-seat-purchase-v1",
items=[
{"external_entity_id": "seat_pro_001", "plan_code": "pro_monthly"},
{"external_entity_id": "seat_max_001", "plan_code": "max_monthly"},
],
)
import { Checkout } from '@nozle-js/react'
if (checkout.type === 'stripe' && checkout.client_secret) {
return (
<Checkout
clientSecret={checkout.client_secret}
publishableKey={import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY}
/>
)
}
POST /customers/workspace_123/entity-subscriptions/checkout
Authorization: Bearer sk_...
Idempotency-Key: workspace-123-seat-purchase-v1
Content-Type: application/json
{
"entity_subscription_checkout": {
"billing_time": "anniversary",
"return_url": "https://app.example.com/settings/billing",
"items": [
{
"external_entity_id": "seat_pro_001",
"plan_code": "pro_monthly"
},
{
"external_entity_id": "seat_max_001",
"plan_code": "max_monthly"
}
]
}
}
{
"entity_subscription_checkout": {
"id": "checkout_batch_uuid",
"type": "stripe",
"status": "open",
"client_secret": "cs_...",
"invoice_id": "invoice_uuid",
"amount_cents": 4498,
"currency": "USD",
"replayed": false,
"items": [
{
"external_entity_id": "seat_pro_001",
"external_subscription_id": "entity-sub-...",
"plan_code": "pro_monthly",
"subscription_status": "incomplete"
}
]
}
}
stripe: mount the returned client secret in Stripe Checkout;processing: Stripe completed, but the verified payment event is still being processed; andcompleted: the invoice is already settled, so no Stripe session is required.
Shared customer billing anchor
Bulk Entity checkout establishes one stable billing calendar for the paying customer:- if the customer has never completed a paid Entity checkout, the first successful payment establishes the anchor;
- failed or abandoned first checkout does not establish an anchor;
- concurrent first-paid checkouts are rejected while the anchor is unresolved;
- later Entity purchases align to the established anchor and prorate their first period; and
- the anchor remains stable even if the customer temporarily has no paid Entities.
billing_time: "anniversary". For example, a successful first payment on August 6 renews on the sixth. A seat added on August 20 is charged only through September 6, then joins the normal renewal invoice.
The anchor is established only after confirmed payment. Creating an invoice or receiving a browser redirect is not enough.
Change plan
POST /customers/workspace_123/entities/user_42/subscription/change-plan
Authorization: Bearer sk_...
Idempotency-Key: change-user-42-max-v1
Content-Type: application/json
{
"plan_code": "max_annual",
"return_url": "https://app.example.com/settings/billing"
}
Cancel
POST /customers/workspace_123/entities/user_42/subscription/cancel
Authorization: Bearer sk_...
Idempotency-Key: cancel-user-42-v1
Content-Type: application/json
{
"timing": "end_of_period"
}
end_of_period unless the merchant explicitly intends to terminate access immediately.
Remove an unused identity
DELETE /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
Stripe webhooks are authoritative for paid activation. Do not grant paid access from a browser redirect or checkout callback alone.