@netiva-ai/billing
v0.0.2
Published
Shared billing primitives for Netiva — SiteConfig, HTTP client, Plans API client, customer authentication, subscription hooks, and Stripe provider. Used by both @netiva-ai/elements and @netiva-ai/site-kit.
Downloads
317
Maintainers
Readme
@netiva-ai/billing
Shared, framework-agnostic billing primitives for Netiva.
This is the single source of truth for workspace-scoped config, customer authentication, plans/subscriptions, and Stripe Elements wiring. It is used internally by:
@netiva-ai/elements— the styled, drop-in React components (<PlanSelector>,<NetivaCheckout>, etc.)@netiva-ai/site-kit— the full in-sandbox runtime for AI-generated sites
You only need to install @netiva-ai/billing directly if you are:
- Building a custom UI on top of Netiva's billing APIs (instead of using the pre-built elements)
- Sharing a single React context instance between
@netiva-ai/elementsand your own components - Consuming the low-level
NetivaPlansClientor HTTP helpers without React
Install
npm install @netiva-ai/billingPeer dependencies
| Package | Required for | Optional? |
| --- | --- | --- |
| react, react-dom (>=18) | All React providers & hooks | No (for the /plans and /stripe subpaths) |
| @stripe/react-stripe-js@^3, @stripe/stripe-js@^5 | <StripeProvider> + useSubscribe | Yes — only if you use the Stripe parts |
The Stripe packages are marked optional in peerDependenciesMeta so a plans-only consumer doesn't pull in the Stripe bundle.
Subpath exports
| Import | Purpose | React? |
| --- | --- | --- |
| @netiva-ai/billing/core | SiteConfig, SiteConfigContext, useSiteConfig, request (HTTP), useAsyncResource, cn, formatPrice | Core hooks only |
| @netiva-ai/billing/plans | NetivaPlansClient, CustomerAuthProvider, PlansProvider, usePlans, useCurrentSubscription, useCustomerAuth, useMagicLinkRequest/useMagicLinkExchange, useSubscribe, types | Yes |
| @netiva-ai/billing/stripe | StripeProvider (loads the workspace's publishable key automatically) | Yes |
The root entry re-exports everything for convenience, but prefer the subpath imports in production bundles.
Quick start (custom UI)
import { NetivaProvider } from '@netiva-ai/elements'; // or compose manually
import { usePlans, useCurrentSubscription, useSubscribe } from '@netiva-ai/billing/plans';
import { formatPrice } from '@netiva-ai/billing/core';
import type { PlanPrice, Plan } from '@netiva-ai/billing/plans';
// 1. Wrap your tree once (near the root)
<NetivaProvider
config={{ apiUrl: 'https://api.netiva.ai', workspaceId: 'ws_123' }}
clientToken={shortLivedToken} // optional — see "Auth" below
>
<MyPricingPage />
</NetivaProvider>
// 2. Consume hooks anywhere inside the provider
function MyPricingPage() {
const { data: plans, loading } = usePlans();
const { data: sub } = useCurrentSubscription();
const { subscribe, status, error } = useSubscribe();
if (loading) return <div>Loading plans...</div>;
return (
<div>
{plans?.map((plan) => (
<button key={plan.id} onClick={() => subscribe(plan.prices[0].id)}>
Subscribe to {plan.name} — {formatPrice(plan.prices[0].unit_amount / 100)}
</button>
))}
{sub && <p>Current: {sub.plan?.name} ({sub.status})</p>}
</div>
);
}Runtime config (SiteConfig)
Every call is scoped to a workspace. You pass the minimal config once:
import type { SiteConfig } from '@netiva-ai/billing/core';
const config: SiteConfig = {
apiUrl: 'https://api.netiva.ai', // or your self-hosted URL
workspaceId: 'ws_123',
};The workspace's Stripe publishable key is never part of this config — StripeProvider fetches it lazily from GET /members/:workspaceId/config.
Auth model (the most important concept)
Netiva supports two ways for an end-customer to act on their subscription:
- Magic link (email-based, used by the hosted members portal)
- Short-lived
clientToken(server-minted JWT, used by embedded checkout)
Option A — Magic link flow (no clientToken)
import { CustomerAuthProvider, useMagicLinkRequest, useMagicLinkExchange } from '@netiva-ai/billing/plans';
<CustomerAuthProvider>
<LoginForm />
<CheckoutAfterLogin />
</CustomerAuthProvider>The customer receives an email, clicks the link, and useMagicLinkExchange writes the resulting JWT into localStorage (workspace-scoped).
Option B — Server-minted clientToken (recommended for embedded UIs)
Your backend already knows who the customer is (you created them via the SDK). Mint a short-lived token (≤15 min) and pass it down:
Server (using the official SDK):
const { token } = await netiva.billing.generateSessionToken({
customerId: 'cus_123',
expiresInMinutes: 10,
});Client:
<NetivaProvider config={config} clientToken={token}>
<NetivaCheckout priceId="price_xxx" onCheckoutComplete={...} />
</NetivaProvider>The token authenticates only the three routes needed for embedded checkout (/auth/me, /plans/me, /plans/subscribe). All other customer routes return 403.
Important: Set the token in one place only — either on <NetivaProvider> or on individual components that call authenticateWithToken. Passing two different tokens causes race conditions.
Composing providers manually (advanced)
If you don't want the styled <NetivaProvider> from @netiva-ai/elements, you can compose the stack yourself:
import { SiteConfigContext, type SiteConfig } from '@netiva-ai/billing/core';
import { CustomerAuthProvider, PlansProvider } from '@netiva-ai/billing/plans';
import { StripeProvider } from '@netiva-ai/billing/stripe';
export function MyBillingRoot({ config, clientToken, children }: {
config: SiteConfig;
clientToken?: string;
children: React.ReactNode;
}) {
return (
<SiteConfigContext.Provider value={config}>
<CustomerAuthProvider clientToken={clientToken}>
<StripeProvider>
<PlansProvider>
{children}
</PlansProvider>
</StripeProvider>
</CustomerAuthProvider>
</SiteConfigContext.Provider>
);
}This is exactly what @netiva-ai/elements' NetivaProvider does (plus theming).
Low-level HTTP client
import { request, HttpError } from '@netiva-ai/billing/core';
const plans = await request<Plan[]>(
`${apiUrl}/members/${workspaceId}/plans`
);All React clients (NetivaPlansClient) are thin wrappers around this helper + response mapping.
Type exports
All important types are re-exported from the subpaths:
import type {
Plan, PlanPrice, ActiveSubscription, PlansCustomer,
SubscribeResult, MagicLinkRequestResult, SiteConfig,
} from '@netiva-ai/billing/plans'; // or from '@netiva-ai/billing/core' for SiteConfigCompatibility with @netiva-ai/elements and @netiva-ai/site-kit
Both higher-level packages declare @netiva-ai/billing as a peer dependency and keep it external in their bundler config. This guarantees that there is exactly one copy of SiteConfigContext, CustomerAuthContext, etc. in your bundle — even if you import hooks directly from @netiva-ai/billing while also using components from @netiva-ai/elements.
If you use both @netiva-ai/elements and @netiva-ai/site-kit, install a single compatible version of @netiva-ai/billing. They will then share provider state automatically.
Why the three subpaths?
/corecontains zero React context — safe to import from server components or non-React code./plansand/stripeare client-only (they ship with'use client'directives for Next.js App Router).- The split lets tree-shakers drop Stripe entirely when you only need
usePlans()(public, no auth).
License
MIT
