> ## 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.

# Entity Subscriptions

> Give individual customer Entities independent plans, billing periods, and subscription lifecycles.

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.

```text theme={null}
Customer: workspace_123
├── Entity: user_42  → Pro monthly
└── Entity: user_43  → Max annual
```

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.

## Ensure a subscription identity

```http theme={null}
PUT /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
```

This operation is idempotent. It creates the stable Entity-to-subscription identity but does not select or activate a plan.

## Read subscriptions

Read one Entity subscription:

```http theme={null}
GET /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
```

List all Entity subscriptions for a customer:

```http theme={null}
GET /customers/workspace_123/entity-subscriptions
Authorization: Bearer sk_...
```

The response reports the current and pending plans independently for each Entity:

```json theme={null}
{
  "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

```http theme={null}
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:

```ts theme={null}
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.
```

The Python SDK exposes the same backend operation:

```python theme={null}
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"},
    ],
)
```

On the frontend, pass a Stripe result to the React checkout component:

```tsx theme={null}
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}
    />
  )
}
```

The endpoint below remains available for backends that do not use the Node SDK.

```http theme={null}
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"
      }
    ]
  }
}
```

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.

```json theme={null}
{
  "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"
      }
    ]
  }
}
```

Nozle supports these result types:

* `stripe`: mount the returned client secret in Stripe Checkout;
* `processing`: Stripe completed, but the verified payment event is still being processed; and
* `completed`: the invoice is already settled, so no Stripe session is required.

The idempotency key covers the customer, billing mode, return URL, Entity IDs, and plans. Retry the exact request with the same key. A different basket with the same key returns a conflict.

## 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.

For a fresh seat pool, send `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.

<Info>
  The anchor is established only after confirmed payment. Creating an invoice or receiving a browser redirect is not enough.
</Info>

## Change plan

```http theme={null}
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"
}
```

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.

## Cancel

```http theme={null}
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"
}
```

Cancellation supports the same settlement controls as customer subscriptions. Prefer `end_of_period` unless the merchant explicitly intends to terminate access immediately.

## Remove an unused identity

```http theme={null}
DELETE /customers/workspace_123/entities/user_42/subscription
Authorization: Bearer sk_...
```

Removal is allowed only when the identity has no active, pending, or incomplete subscription. Historical billing records remain immutable.

<Warning>
  Stripe webhooks are authoritative for paid activation. Do not grant paid access from a browser redirect or checkout callback alone.
</Warning>
