mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
Update payments into concept documents
This commit is contained in:
+12
-1
@@ -131,7 +131,14 @@
|
||||
"icon": "/images/app-icons/payments.svg",
|
||||
"pages": [
|
||||
"guides/apps/payments/overview",
|
||||
"guides/apps/payments/guide"
|
||||
"guides/apps/payments/setup",
|
||||
"guides/apps/payments/products-and-pricing",
|
||||
"guides/apps/payments/items-and-entitlements",
|
||||
"guides/apps/payments/checkout",
|
||||
"guides/apps/payments/subscriptions",
|
||||
"guides/apps/payments/billing-and-invoices",
|
||||
"guides/apps/payments/granting-products",
|
||||
"guides/apps/payments/customers"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -331,6 +338,10 @@
|
||||
{
|
||||
"source": "/guides/apps/fraud-protection/overview",
|
||||
"destination": "/guides/apps/authentication/fraud-protection"
|
||||
},
|
||||
{
|
||||
"source": "/guides/apps/payments/guide",
|
||||
"destination": "/guides/apps/payments/setup"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "Billing & Invoices"
|
||||
description: "Save payment methods and display a customer's billing history"
|
||||
---
|
||||
|
||||
## Payment methods
|
||||
|
||||
Customers can save a payment method for future purchases and plan switches. This is built on a secure setup-intent flow.
|
||||
|
||||
```typescript
|
||||
// Create a setup intent
|
||||
const setupIntent = await user.createPaymentMethodSetupIntent();
|
||||
// setupIntent.clientSecret - use with the embedded card form to collect card details
|
||||
|
||||
// After the user completes the card form:
|
||||
const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent(
|
||||
setupIntentId
|
||||
);
|
||||
// paymentMethod contains: id, brand, last4, exp_month, exp_year
|
||||
// (it may be null, and the card fields can individually be null)
|
||||
```
|
||||
|
||||
To check if a customer has a payment method saved:
|
||||
|
||||
```typescript
|
||||
// Client component (hook)
|
||||
const billing = user.useBilling();
|
||||
|
||||
// Server component
|
||||
const billing = await user.getBilling();
|
||||
|
||||
// billing.hasCustomer - whether a billing customer exists
|
||||
// billing.defaultPaymentMethod - card details or null
|
||||
```
|
||||
|
||||
## Invoices
|
||||
|
||||
List a customer's invoices for displaying billing history:
|
||||
|
||||
```typescript
|
||||
// Client component (hook)
|
||||
const invoices = user.useInvoices();
|
||||
|
||||
// Server component
|
||||
const invoices = await user.listInvoices();
|
||||
```
|
||||
|
||||
Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`, or `null`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to the hosted invoice page, or `null`).
|
||||
|
||||
Invoices support pagination:
|
||||
|
||||
```typescript
|
||||
const firstPage = await user.listInvoices({ limit: 10 });
|
||||
const secondPage = await user.listInvoices({
|
||||
limit: 10,
|
||||
cursor: firstPage.nextCursor,
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Billing and invoices are available for user and team customers only, not custom customers (see [Customer Types](./customers)).
|
||||
</Info>
|
||||
|
||||
## Related
|
||||
|
||||
- [Subscriptions](./subscriptions) - switching plans requires a saved payment method
|
||||
- [Checkout & Purchases](./checkout) - sell a product
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: "Checkout & Purchases"
|
||||
description: "Sell a product with a checkout URL and read what a customer owns"
|
||||
---
|
||||
|
||||
To sell a product, generate a checkout URL and redirect the customer to it. Hexclave runs the hosted checkout, receives the payment confirmation, and grants the product - you never write a webhook handler.
|
||||
|
||||
## Selling a product
|
||||
|
||||
The `createCheckoutUrl` method is available on both user and team objects.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Client Component">
|
||||
```typescript title="app/components/purchase-button.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function PurchaseButton({ productId }: { productId: string }) {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
|
||||
const handlePurchase = async () => {
|
||||
const checkoutUrl = await user.createCheckoutUrl({
|
||||
productId,
|
||||
returnUrl: window.location.href,
|
||||
});
|
||||
window.location.href = checkoutUrl;
|
||||
};
|
||||
|
||||
return <button onClick={handlePurchase}>Purchase</button>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Server Component">
|
||||
```typescript title="app/purchase/page.tsx"
|
||||
import { hexclaveServerApp } from "@/stack/server";
|
||||
|
||||
export default async function PurchasePage() {
|
||||
const user = await hexclaveServerApp.getUser({ or: 'redirect' });
|
||||
|
||||
const checkoutUrl = await user.createCheckoutUrl({
|
||||
productId: "prod_premium_monthly",
|
||||
});
|
||||
|
||||
return <a href={checkoutUrl}>Upgrade to Premium</a>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
For team purchases, call `createCheckoutUrl` on the team object instead:
|
||||
|
||||
```typescript
|
||||
const team = user.useTeam(teamId);
|
||||
const checkoutUrl = await team.createCheckoutUrl({ productId });
|
||||
```
|
||||
|
||||
<Info>
|
||||
If you're using a non-JS backend (Python, Go, etc.), call the REST API directly: `POST /api/v1/payments/purchases/create-purchase-url` with `customer_type`, `customer_id`, and `product_id`. See the [REST API overview](/api/overview) for details.
|
||||
</Info>
|
||||
|
||||
## Checking what a customer owns
|
||||
|
||||
After a purchase, you'll want to know what the customer has. Check their product list, or check a specific [item balance](./items-and-entitlements).
|
||||
|
||||
```typescript
|
||||
// Client component (hook - re-renders on changes)
|
||||
const products = user.useProducts();
|
||||
|
||||
// Server component
|
||||
const products = await user.listProducts();
|
||||
```
|
||||
|
||||
Each product in the list includes:
|
||||
|
||||
- `id` - The product ID (or `null` for inline products)
|
||||
- `displayName` - The product name
|
||||
- `quantity` - How many the customer owns (relevant for stackable products)
|
||||
- `subscription` - `null` for one-time products, or an object with `subscriptionId`, `currentPeriodEnd`, `cancelAtPeriodEnd`, and `isCancelable` for subscriptions
|
||||
- `switchOptions` - Other products in the same product line the customer could switch to
|
||||
|
||||
## Related
|
||||
|
||||
- [Items & Entitlements](./items-and-entitlements) - read and consume credits, seats, and quota
|
||||
- [Subscriptions](./subscriptions) - manage recurring plans after purchase
|
||||
- [Granting Products](./granting-products) - give access without checkout
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: "Customer Types"
|
||||
description: "Bill users, teams, and custom external customers"
|
||||
---
|
||||
|
||||
A **customer** is whoever owns a purchase. Hexclave supports three types:
|
||||
|
||||
- **Users** - Individual user accounts. Users can manage their own purchases, billing, and invoices from the client SDK.
|
||||
- **Teams** - Team or organization accounts. Team admins can create checkouts, switch plans, and cancel subscriptions for their team.
|
||||
- **Custom customers** - External entities identified by an arbitrary string ID. Useful for integrations with external systems. Custom customers are managed only via the server SDK and do not support billing or invoices.
|
||||
|
||||
## Where each method lives
|
||||
|
||||
The full billing surface - [`createCheckoutUrl`](./checkout), [`useProducts`](./checkout#checking-what-a-customer-owns), [`useItem`](./items-and-entitlements), [`switchSubscription`](./subscriptions), [`useBilling` and `useInvoices`](./billing-and-invoices) - is available on both **user** and **team** objects.
|
||||
|
||||
For **custom customers**, use the top-level `hexclaveServerApp` methods with `customCustomerId` (for example [`grantProduct`](./granting-products), `getItem`, and `listProducts`). Billing, invoices, and subscription switching are not available for custom customers.
|
||||
|
||||
Every product declares a **customer type** when you [define it](./products-and-pricing#defining-products), which determines who can purchase it.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: "Granting Products"
|
||||
description: "Give a customer a product without going through checkout"
|
||||
---
|
||||
|
||||
Sometimes you need to give a customer a product without charging them - free trials, admin grants, promotional offers, support credits. Use `grantProduct` on the server.
|
||||
|
||||
## Granting a configured product
|
||||
|
||||
```typescript
|
||||
// Grant a pre-configured product to a user
|
||||
await hexclaveServerApp.grantProduct({
|
||||
userId: "user-id",
|
||||
productId: "prod_premium",
|
||||
});
|
||||
|
||||
// Grant to a team
|
||||
await hexclaveServerApp.grantProduct({
|
||||
teamId: "team-id",
|
||||
productId: "prod_team_plan",
|
||||
});
|
||||
|
||||
// Grant to a custom customer
|
||||
await hexclaveServerApp.grantProduct({
|
||||
customCustomerId: "external-org-123",
|
||||
productId: "prod_enterprise",
|
||||
});
|
||||
```
|
||||
|
||||
If you already have a reference to a **server** user or team object (for example from `hexclaveServerApp.getUser(userId)`), you can also call `user.grantProduct({ productId })` directly. This method is server-only.
|
||||
|
||||
## Inline products
|
||||
|
||||
You can also grant products with an **inline definition** - no pre-configured product needed. This is useful for one-off grants like bonus credits:
|
||||
|
||||
```typescript
|
||||
await hexclaveServerApp.grantProduct({
|
||||
userId: "user-id",
|
||||
product: {
|
||||
display_name: "Bonus Credits",
|
||||
customer_type: "user",
|
||||
server_only: true,
|
||||
stackable: false,
|
||||
prices: {
|
||||
manual: { USD: "0" },
|
||||
},
|
||||
included_items: {
|
||||
credits: { quantity: 100 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Items & Entitlements](./items-and-entitlements) - the credits and seats a grant hands out
|
||||
- [Customer Types](./customers) - grant to users, teams, or custom customers
|
||||
@@ -1,417 +0,0 @@
|
||||
---
|
||||
title: "Payments"
|
||||
description: "Accept payments and manage billing with Hexclave's Payments app"
|
||||
sidebarTitle: "Guide"
|
||||
---
|
||||
|
||||
import { PaymentsConcepts } from "/snippets/payments-concepts.jsx";
|
||||
|
||||
Hexclave includes a Payments app that handles billing, subscriptions, and one-time purchases. Instead of building your own billing system, you define products in the dashboard and Hexclave takes care of checkout, entitlement tracking, subscription lifecycle, and invoicing.
|
||||
|
||||
This guide walks through how to set up payments, sell products, manage subscriptions, and work with item-based entitlements like credits or seats.
|
||||
|
||||
## Getting started
|
||||
|
||||
<Steps>
|
||||
<Step title="Enable Payments">
|
||||
Go to the **Apps** section in your dashboard, find **Payments**, and enable it.
|
||||
</Step>
|
||||
<Step title="Connect your payment account">
|
||||
Open **Payments -> Settings** and follow the onboarding flow. You'll be asked for business details, bank info, and identity verification. Once approved, payments are live.
|
||||
</Step>
|
||||
<Step title="Turn on test mode">
|
||||
While building, enable **test mode** in **Payments -> Settings**. All purchases will be free - no real money is charged. You can switch to live when you're ready.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
Hexclave Payments is currently only available for US-based businesses. Support for other countries is coming soon.
|
||||
</Info>
|
||||
|
||||
## Core concepts
|
||||
|
||||
Before writing any code, it helps to understand how the pieces fit together.
|
||||
|
||||
A **product** is something you sell - a subscription plan, a one-time purchase, or a credit pack. Products can have one or more **prices** (one-time or recurring, in multiple currencies), and they can include **items** - quantifiable entitlements like credits, seats, or API calls.
|
||||
|
||||
A **product line** groups products that are mutually exclusive. For example, a "Plan" product line might contain Free, Pro, and Enterprise - a customer can only have one at a time. When they upgrade, the old plan is replaced.
|
||||
|
||||
A **customer** is whoever owns the purchase. This can be a user, a team, or a custom external entity.
|
||||
|
||||
Here's how these pieces look in practice:
|
||||
|
||||
<PaymentsConcepts />
|
||||
|
||||
And the typical flow to make it all work:
|
||||
|
||||
1. You define products and items in the dashboard
|
||||
2. Your app generates a checkout URL and redirects the user to the hosted checkout page
|
||||
3. The user pays, Hexclave is notified, and the product is granted
|
||||
4. Your app reads the customer's products and item balances to control access
|
||||
|
||||
## Defining products
|
||||
|
||||
Configure your products in **Payments -> Products & Items**. Each product has:
|
||||
|
||||
- **Display name** - What the customer sees
|
||||
- **Customer type** - Whether this product is for users, teams, or custom customers
|
||||
- **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Currency amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Supported currencies: USD, EUR, GBP, JPY, INR, AUD, CAD.
|
||||
- **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior
|
||||
|
||||
A few additional options:
|
||||
|
||||
- **Product lines** - Assign products to a product line to make them mutually exclusive (e.g. plan tiers). Configure lines in **Payments -> Product Lines**.
|
||||
- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one.
|
||||
- **Free trial** - Give customers a trial period before charging. Can be set on the product or on individual prices.
|
||||
- **Server-only** - Hide the product from client SDK responses. Useful for products that should only be granted programmatically.
|
||||
- **Stackable** - Allow multiple purchases of the same product (default is one per customer).
|
||||
|
||||
## Selling a product
|
||||
|
||||
To sell a product, generate a checkout URL and redirect the user to it. The `createCheckoutUrl` method is available on both user and team objects.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Client Component">
|
||||
```typescript title="app/components/purchase-button.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function PurchaseButton({ productId }: { productId: string }) {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
|
||||
const handlePurchase = async () => {
|
||||
const checkoutUrl = await user.createCheckoutUrl({
|
||||
productId,
|
||||
returnUrl: window.location.href,
|
||||
});
|
||||
window.location.href = checkoutUrl;
|
||||
};
|
||||
|
||||
return <button onClick={handlePurchase}>Purchase</button>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Server Component">
|
||||
```typescript title="app/purchase/page.tsx"
|
||||
import { hexclaveServerApp } from "@/stack/server";
|
||||
|
||||
export default async function PurchasePage() {
|
||||
const user = await hexclaveServerApp.getUser({ or: 'redirect' });
|
||||
|
||||
const checkoutUrl = await user.createCheckoutUrl({
|
||||
productId: "prod_premium_monthly",
|
||||
});
|
||||
|
||||
return <a href={checkoutUrl}>Upgrade to Premium</a>;
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
For team purchases, call `createCheckoutUrl` on the team object instead:
|
||||
|
||||
```typescript
|
||||
const team = user.useTeam(teamId);
|
||||
const checkoutUrl = await team.createCheckoutUrl({ productId });
|
||||
```
|
||||
|
||||
<Info>
|
||||
If you're using a non-JS backend (Python, Go, etc.), call the REST API directly: `POST /api/v1/payments/purchases/create-purchase-url` with `customer_type`, `customer_id`, and `product_id`. See the [REST API overview](/api/overview) for details.
|
||||
</Info>
|
||||
|
||||
## Checking what a customer owns
|
||||
|
||||
After a purchase, you'll want to know what the customer has. There are two ways to do this: check their product list, or check a specific item balance.
|
||||
|
||||
### Listing products
|
||||
|
||||
```typescript
|
||||
// Client component (hook - re-renders on changes)
|
||||
const products = user.useProducts();
|
||||
|
||||
// Server component
|
||||
const products = await user.listProducts();
|
||||
```
|
||||
|
||||
Each product in the list includes:
|
||||
|
||||
- `id` - The product ID (or `null` for inline products)
|
||||
- `displayName` - The product name
|
||||
- `quantity` - How many the customer owns (relevant for stackable products)
|
||||
- `type` - `"one_time"` or `"subscription"`
|
||||
- `subscription` - Subscription details (period end, cancel state) if applicable
|
||||
- `switchOptions` - Other products in the same product line the customer could switch to
|
||||
|
||||
### Checking item balances
|
||||
|
||||
Items are the building blocks of entitlements. When a product includes items like "100 credits" or "5 seats", those are tracked as item balances on the customer.
|
||||
|
||||
```typescript
|
||||
// Client component (hook - re-renders on changes)
|
||||
const credits = user.useItem("credits");
|
||||
|
||||
// Server component
|
||||
const credits = await user.getItem("credits");
|
||||
```
|
||||
|
||||
An item has two quantity fields:
|
||||
|
||||
- `quantity` - The raw balance (can be negative if you've consumed more than granted)
|
||||
- `nonNegativeQuantity` - `Math.max(0, quantity)` for display purposes
|
||||
|
||||
Here's a practical example - showing a credits counter:
|
||||
|
||||
```typescript title="app/components/credits-widget.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function CreditsWidget() {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const credits = user.useItem("credits");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Available Credits</h3>
|
||||
<p>{credits.nonNegativeQuantity}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Consuming credits (server-side)
|
||||
|
||||
When your app needs to consume credits (e.g. when a user sends an AI request), use `tryDecreaseQuantity` on the server. It's atomic and race-condition-safe - it will return `false` and do nothing if the balance would go negative.
|
||||
|
||||
```typescript title="lib/credits.ts"
|
||||
import { hexclaveServerApp } from "@/stack/server";
|
||||
|
||||
export async function consumeCredits(userId: string, amount: number) {
|
||||
const user = await hexclaveServerApp.getUser(userId);
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
const credits = await user.getItem("credits");
|
||||
const success = await credits.tryDecreaseQuantity(amount);
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Insufficient credits");
|
||||
}
|
||||
|
||||
return { remaining: credits.quantity };
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Always use `tryDecreaseQuantity()` instead of checking the balance and then decreasing. This prevents race conditions where multiple requests could consume more credits than available.
|
||||
</Info>
|
||||
|
||||
You can also increase a balance with `credits.increaseQuantity(amount)` or decrease without the safety check using `credits.decreaseQuantity(amount)`.
|
||||
|
||||
## Managing subscriptions
|
||||
|
||||
### Switching plans
|
||||
|
||||
When products belong to the same product line, customers can switch between them. For example, upgrading from Pro to Enterprise:
|
||||
|
||||
```typescript title="app/components/upgrade-button.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function UpgradeButton() {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
|
||||
return (
|
||||
<button onClick={async () => {
|
||||
await user.switchSubscription({
|
||||
fromProductId: "prod_pro",
|
||||
toProductId: "prod_enterprise",
|
||||
});
|
||||
}}>
|
||||
Upgrade to Enterprise
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Switching plans requires the customer to have a default payment method saved. The switch happens immediately and the charge is prorated automatically.
|
||||
</Info>
|
||||
|
||||
### Canceling a subscription
|
||||
|
||||
`cancelSubscription` is called on the app instance rather than the user object:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Client Component">
|
||||
```typescript
|
||||
"use client";
|
||||
import { useHexclaveApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function CancelButton({ productId }: { productId: string }) {
|
||||
const app = useHexclaveApp();
|
||||
return (
|
||||
<button onClick={async () => {
|
||||
await app.cancelSubscription({ productId });
|
||||
}}>
|
||||
Cancel Subscription
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Server">
|
||||
```typescript
|
||||
// Cancel for the current user
|
||||
await hexclaveServerApp.cancelSubscription({ productId: "prod_pro" });
|
||||
|
||||
// Cancel for a team
|
||||
await hexclaveServerApp.cancelSubscription({
|
||||
productId: "prod_team_plan",
|
||||
teamId: "team-id",
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Billing and payment methods
|
||||
|
||||
Customers can save a payment method for future purchases and plan switches. This is built on a secure setup-intent flow.
|
||||
|
||||
```typescript
|
||||
// Create a setup intent
|
||||
const setupIntent = await user.createPaymentMethodSetupIntent();
|
||||
// setupIntent.clientSecret - use with the embedded card form to collect card details
|
||||
// setupIntent.stripeAccountId - the connected payment account ID
|
||||
|
||||
// After the user completes the card form:
|
||||
const paymentMethod = await user.setDefaultPaymentMethodFromSetupIntent(
|
||||
setupIntentId
|
||||
);
|
||||
// paymentMethod contains: id, brand, last4, exp_month, exp_year
|
||||
```
|
||||
|
||||
To check if a customer has a payment method saved:
|
||||
|
||||
```typescript
|
||||
// Client component (hook)
|
||||
const billing = user.useBilling();
|
||||
|
||||
// Server component
|
||||
const billing = await user.getBilling();
|
||||
|
||||
// billing.hasCustomer - whether a billing customer exists
|
||||
// billing.defaultPaymentMethod - card details or null
|
||||
```
|
||||
|
||||
## Invoices
|
||||
|
||||
List a customer's invoices for displaying billing history:
|
||||
|
||||
```typescript
|
||||
// Client component (hook)
|
||||
const invoices = user.useInvoices();
|
||||
|
||||
// Server component
|
||||
const invoices = await user.listInvoices();
|
||||
```
|
||||
|
||||
Each invoice includes `createdAt`, `status` (`"draft"`, `"open"`, `"paid"`, `"uncollectible"`, `"void"`), `amountTotal` (in cents), and `hostedInvoiceUrl` (a link to the hosted invoice page).
|
||||
|
||||
Invoices support pagination:
|
||||
|
||||
```typescript
|
||||
const firstPage = await user.listInvoices({ limit: 10 });
|
||||
const secondPage = await user.listInvoices({
|
||||
limit: 10,
|
||||
cursor: firstPage.nextCursor,
|
||||
});
|
||||
```
|
||||
|
||||
<Info>
|
||||
Invoices are available for user and team customers only, not custom customers.
|
||||
</Info>
|
||||
|
||||
## Granting products programmatically
|
||||
|
||||
Sometimes you need to give a customer a product without going through checkout - free trials, admin grants, promotional offers, etc. Use `grantProduct` on the server:
|
||||
|
||||
```typescript
|
||||
// Grant a pre-configured product to a user
|
||||
await hexclaveServerApp.grantProduct({
|
||||
userId: "user-id",
|
||||
productId: "prod_premium",
|
||||
});
|
||||
|
||||
// Grant to a team
|
||||
await hexclaveServerApp.grantProduct({
|
||||
teamId: "team-id",
|
||||
productId: "prod_team_plan",
|
||||
});
|
||||
|
||||
// Grant to a custom customer
|
||||
await hexclaveServerApp.grantProduct({
|
||||
customCustomerId: "external-org-123",
|
||||
productId: "prod_enterprise",
|
||||
});
|
||||
```
|
||||
|
||||
You can also grant products with an **inline definition** - no pre-configured product needed. This is useful for one-off grants like bonus credits:
|
||||
|
||||
```typescript
|
||||
await hexclaveServerApp.grantProduct({
|
||||
userId: "user-id",
|
||||
product: {
|
||||
display_name: "Bonus Credits",
|
||||
customer_type: "user",
|
||||
server_only: true,
|
||||
stackable: false,
|
||||
prices: {
|
||||
manual: { USD: "0" },
|
||||
},
|
||||
included_items: {
|
||||
credits: { quantity: 100 },
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you have a reference to the user object already, you can also call `user.grantProduct({ productId })` directly.
|
||||
|
||||
## Customer types
|
||||
|
||||
Hexclave supports three types of payment customers:
|
||||
|
||||
- **Users** - Individual user accounts. Users can manage their own purchases, billing, and invoices from the client SDK.
|
||||
- **Teams** - Team or organization accounts. Team admins can create checkouts, switch plans, and cancel subscriptions for their team.
|
||||
- **Custom customers** - External entities identified by an arbitrary string ID. Useful for integrations with external systems. Custom customers can only be managed via the server SDK and do not support billing or invoices.
|
||||
|
||||
All payment methods (`createCheckoutUrl`, `useProducts`, `useItem`, `switchSubscription`, `useBilling`, `useInvoices`, etc.) are available on both user and team objects. For custom customers, use the top-level `hexclaveServerApp` methods with `customCustomerId`.
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard gives you full visibility and control over your payments:
|
||||
|
||||
- **Product Lines** - Group products into mutually exclusive tiers. Configure in **Payments -> Product Lines**.
|
||||
- **Products & Items** - Create and edit products, set pricing, and configure included items. In **Payments -> Products & Items**.
|
||||
- **Customers** - View item balances per customer, manually adjust quantities (with optional expiration), and grant products directly. In **Payments -> Customers**.
|
||||
- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**.
|
||||
- **Payouts** - View payout information. In **Payments -> Payouts**.
|
||||
- **Settings** - Connect your payment account, toggle test mode, configure payment methods, and block new purchases. In **Payments -> Settings**.
|
||||
|
||||
### Payment emails
|
||||
|
||||
Email notifications are sent automatically on payment events:
|
||||
|
||||
- **Payment Receipt** - Sent on successful payment with product details, amount, and receipt link
|
||||
- **Payment Failed** - Sent on failed payment with product name, amount, and failure reason
|
||||
|
||||
These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates**.
|
||||
|
||||
## Test mode
|
||||
|
||||
During development, enable test mode in **Payments -> Settings**. All purchases will be free and no real money is charged - products are granted immediately without going through the payment processor.
|
||||
|
||||
When you're ready to test with real payment flows (but still using test credentials), disable Hexclave's test mode and use these test card numbers:
|
||||
|
||||
- **Success**: `4242 4242 4242 4242`
|
||||
- **Decline**: `4000 0000 0000 0002`
|
||||
- **Insufficient Funds**: `4000 0000 0000 9995`
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: "Items & Entitlements"
|
||||
description: "Track and consume credits, seats, and API quota with race-safe item balances"
|
||||
---
|
||||
|
||||
Items are the building blocks of entitlements. Instead of just recording that a customer "bought the Pro plan," Hexclave tracks the quantifiable things that plan grants - **credits**, **seats**, **API calls** - as item balances on the customer, and keeps them in sync as customers buy, consume, renew, and churn.
|
||||
|
||||
When a product includes items like "100 credits" or "5 seats", those quantities are granted on purchase. Each included item is configured with:
|
||||
|
||||
- **Quantity** - How much to grant
|
||||
- **Repeat** - An optional refresh interval (e.g. grant again every month), or `never`
|
||||
- **Expires** - When the grant expires: `never`, `when-purchase-expires`, or `when-repeated`
|
||||
|
||||
## Checking item balances
|
||||
|
||||
```typescript
|
||||
// Client component (hook - re-renders on changes)
|
||||
const credits = user.useItem("credits");
|
||||
|
||||
// Server component
|
||||
const credits = await user.getItem("credits");
|
||||
```
|
||||
|
||||
An item has two quantity fields:
|
||||
|
||||
- `quantity` - The raw balance (can be negative if you've consumed more than granted)
|
||||
- `nonNegativeQuantity` - `Math.max(0, quantity)` for display purposes
|
||||
|
||||
Here's a practical example - showing a credits counter:
|
||||
|
||||
```typescript title="app/components/credits-widget.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function CreditsWidget() {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
const credits = user.useItem("credits");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Available Credits</h3>
|
||||
<p>{credits.nonNegativeQuantity}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Consuming credits (server-side)
|
||||
|
||||
When your app needs to consume credits (e.g. when a user sends an AI request), use `tryDecreaseQuantity` on the server. It's a single transactional operation - it returns `false` and does nothing if the balance would go negative, so concurrent requests can't overspend.
|
||||
|
||||
```typescript title="lib/credits.ts"
|
||||
import { hexclaveServerApp } from "@/stack/server";
|
||||
|
||||
export async function consumeCredits(userId: string, amount: number) {
|
||||
const user = await hexclaveServerApp.getUser(userId);
|
||||
if (!user) throw new Error("User not found");
|
||||
|
||||
const credits = await user.getItem("credits");
|
||||
const success = await credits.tryDecreaseQuantity(amount);
|
||||
|
||||
if (!success) {
|
||||
throw new Error("Insufficient credits");
|
||||
}
|
||||
|
||||
return { remaining: credits.quantity };
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Always use `tryDecreaseQuantity()` instead of checking the balance and then decreasing. This prevents race conditions where multiple requests could consume more credits than available.
|
||||
</Info>
|
||||
|
||||
You can also increase a balance with `credits.increaseQuantity(amount)` or decrease without the safety check using `credits.decreaseQuantity(amount)`. These consumption methods are **server-only** - client-side `useItem` returns a read-only balance.
|
||||
|
||||
## Adjusting balances from the dashboard
|
||||
|
||||
In **Payments -> Customers** you can view item balances per customer and manually adjust quantities (with an optional expiration) - useful for support credits, comps, or corrections.
|
||||
|
||||
## Related
|
||||
|
||||
- [Products & Pricing](./products-and-pricing) - attach items to a product
|
||||
- [Granting Products](./granting-products) - grant a one-off bundle of items
|
||||
@@ -6,7 +6,7 @@ sidebarTitle: "Overview"
|
||||
|
||||
<Visibility for="agents">
|
||||
<Note>
|
||||
**For agents/LLMs:** This is a high-level *marketing* overview of the Payments app, not an implementation reference. To actually build with payments, use [Setup](/guides/getting-started/setup) and the [Payments guide](./guide) (products, checkout, items/entitlements, subscriptions, billing, invoices, and granting products).
|
||||
**For agents/LLMs:** This is a high-level *marketing* overview of the Payments app, not an implementation reference. To actually build with payments, start at [Payments setup](./setup), then use the concept pages: [Products & Pricing](./products-and-pricing), [Items & Entitlements](./items-and-entitlements), [Checkout & Purchases](./checkout), [Subscriptions](./subscriptions), [Billing & Invoices](./billing-and-invoices), [Granting Products](./granting-products), and [Customer Types](./customers).
|
||||
</Note>
|
||||
</Visibility>
|
||||
|
||||
@@ -108,4 +108,4 @@ Straight answers so there are no surprises:
|
||||
2. Connect your payment account under **Payments → Settings**, and turn on **test mode** while you build.
|
||||
3. Define a product (with items, if you want entitlements), then call `user.createCheckoutUrl(...)`.
|
||||
|
||||
Ready for the details - products, items, subscriptions, billing, invoices, and grants? Read the [Payments guide](./guide).
|
||||
Ready for the details - products, items, subscriptions, billing, invoices, and grants? Start with [Payments setup](./setup).
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: "Products & Pricing"
|
||||
description: "Define what you sell - products, prices, product lines, add-ons, and trials"
|
||||
---
|
||||
|
||||
A **product** is anything a customer can buy: a subscription plan, a one-time purchase, or a credit pack. Configure your products in **Payments -> Products & Items**.
|
||||
|
||||
## Defining products
|
||||
|
||||
Each product has:
|
||||
|
||||
- **Display name** - What the customer sees
|
||||
- **Customer type** - Whether this product is for users, teams, or custom customers (see [Customer Types](./customers))
|
||||
- **Prices** - One or more prices, each with a currency amount and an optional billing interval (day, week, month, or year). Amounts are **decimal strings** like `"9.99"` or `"1000"` — not cent integers. Payments are processed in **USD**.
|
||||
- **Included items** - Items granted when the product is purchased, with configurable quantity, repeat schedule, and expiration behavior (see [Items & Entitlements](./items-and-entitlements))
|
||||
|
||||
A few additional options:
|
||||
|
||||
- **Free trial** - Give customers a trial period before charging. Can be set on the product or on individual prices.
|
||||
- **Add-ons** - Set **isAddOnTo** to require the customer to already own a specific base product before purchasing this one.
|
||||
- **Server-only** - Hide the product from client SDK responses. Useful for products that should only be granted programmatically.
|
||||
- **Stackable** - Allow multiple purchases of the same product (default is one per customer).
|
||||
|
||||
## Product lines
|
||||
|
||||
A **product line** groups products that are mutually exclusive. For example, a "Plan" product line might contain Free, Pro, and Enterprise - a customer can only hold one at a time. When they upgrade or downgrade, the old product is replaced.
|
||||
|
||||
Assign products to a product line to power plan tiers and enable [subscription switching](./subscriptions). Configure lines in **Payments -> Product Lines**.
|
||||
|
||||
<Info>
|
||||
Add-ons are exempt from product-line exclusivity - a customer can own an add-on alongside their base product in the same line.
|
||||
</Info>
|
||||
|
||||
## Related
|
||||
|
||||
- [Items & Entitlements](./items-and-entitlements) - attach credits, seats, and quota to a product
|
||||
- [Checkout & Purchases](./checkout) - sell a product once it's defined
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
title: "Setup"
|
||||
description: "Enable Payments, connect a payment account, and understand how the pieces fit together"
|
||||
---
|
||||
|
||||
import { PaymentsConcepts } from "/snippets/payments-concepts.jsx";
|
||||
|
||||
Hexclave includes a Payments app that handles billing, subscriptions, and one-time purchases. Instead of building your own billing system, you define products in the dashboard and Hexclave takes care of checkout, entitlement tracking, subscription lifecycle, and invoicing.
|
||||
|
||||
## Getting started
|
||||
|
||||
<Steps>
|
||||
<Step title="Enable Payments">
|
||||
Go to the **Apps** section in your dashboard, find **Payments**, and enable it.
|
||||
</Step>
|
||||
<Step title="Connect your payment account">
|
||||
Open **Payments -> Settings** and follow the onboarding flow. You'll be asked for business details, bank info, and identity verification. Once approved, payments are live.
|
||||
</Step>
|
||||
<Step title="Turn on test mode">
|
||||
While building, enable **test mode** in **Payments -> Settings**. All purchases will be free - no real money is charged. You can switch to live when you're ready.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
Hexclave Payments is currently only available for US-based businesses, and processes payments in **USD**. Support for other countries and currencies is coming soon.
|
||||
</Info>
|
||||
|
||||
## Core concepts
|
||||
|
||||
Before writing any code, it helps to understand how the pieces fit together.
|
||||
|
||||
A **product** is something you sell - a subscription plan, a one-time purchase, or a credit pack. Products can have one or more **prices** (one-time or recurring), and they can include **items** - quantifiable entitlements like credits, seats, or API calls.
|
||||
|
||||
A **product line** groups products that are mutually exclusive. For example, a "Plan" product line might contain Free, Pro, and Enterprise - a customer can only have one at a time. When they upgrade, the old plan is replaced.
|
||||
|
||||
A **customer** is whoever owns the purchase. This can be a user, a team, or a custom external entity.
|
||||
|
||||
Here's how these pieces look in practice:
|
||||
|
||||
<PaymentsConcepts />
|
||||
|
||||
And the typical flow to make it all work:
|
||||
|
||||
1. You define products and items in the dashboard
|
||||
2. Your app generates a checkout URL and redirects the user to the hosted checkout page
|
||||
3. The user pays, Hexclave is notified, and the product is granted
|
||||
4. Your app reads the customer's products and item balances to control access
|
||||
|
||||
Each step has its own guide:
|
||||
|
||||
- [Products & Pricing](./products-and-pricing) - define what you sell
|
||||
- [Items & Entitlements](./items-and-entitlements) - credits, seats, and quota
|
||||
- [Checkout & Purchases](./checkout) - sell a product and read what a customer owns
|
||||
- [Subscriptions](./subscriptions) - switching plans and cancellation
|
||||
- [Billing & Invoices](./billing-and-invoices) - payment methods and billing history
|
||||
- [Granting Products](./granting-products) - give access without checkout
|
||||
- [Customer Types](./customers) - users, teams, and custom customers
|
||||
|
||||
## Test mode
|
||||
|
||||
While you're building, turn on **test mode** in **Payments -> Settings**. With test mode on, purchases skip the payment processor entirely: products and items are granted instantly, no card is collected, and no money moves. It's on by default in development environments.
|
||||
|
||||
This lets you wire up checkout, entitlements, and subscription logic end to end without touching real money. When you're confident everything works, turn test mode off - at which point purchases go through the payment processor and **charge real money**.
|
||||
|
||||
<Warning>
|
||||
There's no "live but free" middle ground. With test mode **off**, every purchase is a real, billable charge. Keep test mode on until you're ready to take real payments.
|
||||
</Warning>
|
||||
|
||||
## Dashboard
|
||||
|
||||
The dashboard gives you full visibility and control over your payments:
|
||||
|
||||
- **Product Lines** - Group products into mutually exclusive tiers. In **Payments -> Product Lines**.
|
||||
- **Products & Items** - Create and edit products, set pricing, and configure included items. In **Payments -> Products & Items**.
|
||||
- **Customers** - View item balances per customer, manually adjust quantities (with optional expiration), and grant products directly. In **Payments -> Customers**.
|
||||
- **Transactions** - See all payment activity, filter by type and customer, view details, and issue refunds. In **Payments -> Transactions**.
|
||||
- **Payouts** - View payout information. In **Payments -> Payouts**.
|
||||
- **Settings** - Connect your payment account, toggle test mode, configure payment methods, and block new purchases. In **Payments -> Settings**.
|
||||
|
||||
### Payment emails
|
||||
|
||||
Email notifications are sent automatically on payment events:
|
||||
|
||||
- **Payment Receipt** - Sent on successful payment with product details, amount, and receipt link
|
||||
- **Payment Failed** - Sent on failed payment with product name, amount, and failure reason
|
||||
|
||||
These apply to both one-time purchases and subscription renewals. Customize them in **Emails -> Templates** (see the [Emails guide](/guides/apps/emails/guide)).
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Subscriptions"
|
||||
description: "Switch plans with automatic proration, and cancel subscriptions"
|
||||
---
|
||||
|
||||
When products belong to the same [product line](./products-and-pricing#product-lines), customers can move between them, and Hexclave handles the proration and timing for you.
|
||||
|
||||
## Switching plans
|
||||
|
||||
For example, upgrading from Pro to Enterprise:
|
||||
|
||||
```typescript title="app/components/upgrade-button.tsx"
|
||||
"use client";
|
||||
import { useUser } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function UpgradeButton() {
|
||||
const user = useUser({ or: 'redirect' });
|
||||
|
||||
return (
|
||||
<button onClick={async () => {
|
||||
await user.switchSubscription({
|
||||
fromProductId: "prod_pro",
|
||||
toProductId: "prod_enterprise",
|
||||
});
|
||||
}}>
|
||||
Upgrade to Enterprise
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Info>
|
||||
Switching plans requires the customer to have a default payment method saved (see [Billing & Invoices](./billing-and-invoices)). The switch happens immediately and the charge is prorated automatically.
|
||||
</Info>
|
||||
|
||||
## Canceling a subscription
|
||||
|
||||
`cancelSubscription` is called on the app instance rather than the user object:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Client Component">
|
||||
```typescript
|
||||
"use client";
|
||||
import { useHexclaveApp } from "@hexclave/next"; // replace `next` with the correct framework SDK package
|
||||
|
||||
export default function CancelButton({ productId }: { productId: string }) {
|
||||
const app = useHexclaveApp();
|
||||
return (
|
||||
<button onClick={async () => {
|
||||
await app.cancelSubscription({ productId });
|
||||
}}>
|
||||
Cancel Subscription
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Server">
|
||||
```typescript
|
||||
// Cancel for the current user
|
||||
await hexclaveServerApp.cancelSubscription({ productId: "prod_pro" });
|
||||
|
||||
// Cancel for a team
|
||||
await hexclaveServerApp.cancelSubscription({
|
||||
productId: "prod_team_plan",
|
||||
teamId: "team-id",
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Reading subscription state
|
||||
|
||||
Each subscription product returned by [`useProducts` / `listProducts`](./checkout#checking-what-a-customer-owns) carries a `subscription` field with the current period end and cancellation state, plus `switchOptions` listing the other products in the same product line the customer can switch to.
|
||||
|
||||
## Related
|
||||
|
||||
- [Checkout & Purchases](./checkout) - start a subscription
|
||||
- [Billing & Invoices](./billing-and-invoices) - payment methods and billing history
|
||||
Reference in New Issue
Block a user