Skip to main content
Two one-time jobs. Neither is needed for a new integration.

Upgrading from 0.3.x

v2 (0.4.0) is a breaking release. The tables below map every removed or changed API to its replacement.

creem is now a peer dependency

creem moved out of this package’s dependencies, so it is no longer installed for you. Add it to your app or the package will fail to resolve at runtime:
Svelte apps also need Svelte 5.25 or newer. The widgets ship as uncompiled .svelte source and reassign $derived state, which earlier 5.x releases reject at compile time. If you share a billing catalog between server and browser code, import the helpers from @creem_io/convex/core rather than the root entry — the root is the server client and pulls the Creem Node SDK into your browser bundle:

Removed primitives

These exports were deleted. Importing them is a build failure. BillingToggle was a thin wrapper that rendered cycle labels into a SegmentGroup. Inside a Subscription.Root, Subscription.IntervalSelector replaces it and is already wired to the root’s cycles; outside one, compose SegmentGroup directly.

Typed connected API and connectCreemApi

ConnectedBillingApi fields are now typed by function kind, args, and return type, so wiring the wrong generated export fails to compile. Build the object with connectCreemApi(api.billing) instead of by hand.

Resolver returns null for anonymous callers

ApiResolver now returns ResolvedBillingIdentity | null. Returning null is the anonymous signal; a thrown error is a real failure that is logged and rethrown instead of silently degrading a signed-in user to the logged-out view.

subscriptions.update is a discriminated union

Those combinations no longer typecheck, and parseSubscriptionUpdateArgs re-checks them for calls arriving over the wire. A plan switch no longer accepts units: Creem applies a product upgrade and a quantity change as separate operations, so send a second kind: "units" update instead. Unit counts are validated server-side (integer, 1 – 1,000,000).

Widget namespaces are plain objects

<Subscription>, <Product>, and <Credits> are no longer callable components. Subscription and Product used to alias their Item, while Credits aliased its Root. Rendering the bare namespace silently produced the wrong element.

Binding and provider props

createCreemReact and createCreemSvelte no longer return config, which the provider never accepted. The binding is spreadable: PaymentRecoveryButton now reads customers.portalUrl from the provider; portalUrl is an optional override rather than a required prop.

Model and return shapes

subscriptions.getCurrent no longer throws Product not found when a subscription references a product this deployment has not synced yet - that failure took down every query composed on top of it. product is now nullable, so narrow it before use:

Subscription lifecycle behaviour

These are bug fixes rather than API changes, but they change what your app sees at runtime, so review any entitlement code you wrote against 0.3.x.
If your app granted access by checking that a subscription merely exists, users on expired or unpaid subscriptions were silently keeping paid access. Gate on the status instead - isActiveSubscriptionStatus covers the active-like statuses, including the past_due dunning grace period:
Tightening this may revoke access from users who should never have had it, so check your own data before deploying.

Provider-based widget API

Connected widgets no longer accept direct api={billingApi} props. Wrap the area that renders billing UI with CreemConvexProvider and pass the API, catalog, permissions, and consent hooks there. This is intentionally breaking so apps have one billing integration boundary instead of repeating Convex function references throughout the UI.

Unit-based billing rename

The UI widget API now follows Creem’s unit-based pricing language. There are no backward-compatible aliases for the previous seat-specific names. The units prop remains the quantity passed to checkout and subscription updates. A unit can still represent a seat, but the public API no longer assumes that seats are the only unit-based pricing use case.

Pricing columns

twoColumnLayout was removed. Use columns={2} for a fixed two-column pricing grid, or omit it for automatic layout.

Product namespace cleanup

Product.Group was removed. It was only a compatibility alias for Product.Root, not a real grouping primitive. Render Product.Root directly.

Subscription.Group is no longer an alias for Subscription.Root

This one fails silently. In 0.3.x Subscription.Group was a compatibility alias for Subscription.Root. It is now a distinct component that renders its children only while its value matches the root’s active group.Used as a root, it provides no root context and no plans, so it renders nothing at all — no error, no warning, just an empty pricing section.

PricingSection props

PricingSection is a public primitive, and its props changed beyond the seat→unit rename. (PricingCard already took activePlanId in 0.3.x and is unaffected.)

AvailableAction covers billing state only

switch_interval, update_units, and contact_sales were removed from AvailableAction and from snapshot.availableBillingActions. They described the shape of a plan, not the entity’s billing state, and the v2 resolver never emitted them — so a BillingGate requiring one already rendered only its fallback. Removing them turns that silent no-op into a compile error. Answer those questions from the catalog entry instead: checkout, portal, cancel, and reactivate are unchanged.

cycleBadges merges instead of replacing

<Subscription.IntervalSelector cycleBadges={...} /> in React used to replace the root’s badge map wholesale, so setting one interval’s badge silently dropped the rest. It now merges per interval, falling back to the root’s badge for any interval the prop omits — matching Svelte, which already behaved this way. To keep the old “replace everything” behaviour, pass a badge for every interval you render.

orders.list returns unpaid orders

orders.list previously filtered to status === "paid". It now returns every one-time order, including pending, refunded, and partially_refunded ones, so the caller decides what counts as owned. snapshot.access and ownedProductIds still apply the paid filter for you. If you treated everything orders.list returned as granting access, filter on status explicitly:

App-owned plans and i18n

freePlans.activate and onBeforeFreePlanActivation were removed: Use the new plan-based API for free, trial, or other app-owned catalog plans. Convex-Creem writes snapshot.appPlanAssignments for the current app-owned plan; host apps only need a custom activePlanId projection when they intentionally want to override the component-managed assignment. Price interval suffixes now come from labels.priceInterval, so override those labels instead of formatting /mo or /yr in application code.

App-plan eligibility helpers

isAppPlanEligible and shouldShowPlan now take an eligibility context object as their third argument. Pass activePlanId, activeOrScheduledPlanIds, and catalogPlans explicitly so once-per-entity and scoped trial rules evaluate against the same billing state as the widgets. For scoped trials, include activeOrScheduledPlanIds and catalogPlans:

Billing Snapshot Contract

creem.getBillingSnapshot(...) and the generated creem.api({ resolve }).snapshot query return the canonical BillingSnapshot shape with explicit arrays for subscriptions, one-time orders, and app-owned plan assignments, plus a derived access projection. The generated uiModel used by connected widgets exposes the same canonical snapshot as uiModel.snapshot.

Product credit grants

Product.Item no longer accepts checkout metadata for credit grants. Put purchase-to-credit mappings in the server-owned billing catalog with creditGrant so webhook fulfillment derives the amount from trusted app code. By default, catalog-granted credits are reversed only on full refunds. Configure refundBehavior per product when your app needs proportional ("prorate"), eager ("debit"), or manual ("none") refund handling.

Retiring a previous billing provider

Moving from another billing integration (Stripe, LemonSqueezy, a custom setup)? The component’s setup is additive, but removing the old system safely needs a manual inventory:
  1. Old webhook endpoints, both in your app and in the old provider’s dashboard (/api/stripe/webhook, /webhooks/lemonsqueezy, and so on). Keep them only while another live flow depends on them.
  2. Client env vars: old product, price, plan, and publishable-key variables in the VITE_*, NEXT_PUBLIC_*, and PUBLIC_* namespaces.
  3. Server env vars: old API keys, webhook secrets, portal config, and price IDs. These hide in several places at once, including local .env* files, hosting provider settings, CI secrets, and the old dashboard.
  4. Background jobs: scheduled syncs or functions still reading old billing env vars or processing old webhook events.
Switch traffic only after the Creem webhook is verifiably receiving events, and don’t delete old secrets before rollback and data-retention requirements are clear. The Integration Guide includes this checklist in an agent-executable form.