Skip to main content

Documentation Index

Fetch the complete documentation index at: https://stackauth-e0affa27-apps-support.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Stack Auth includes a Payments app that handles billing, subscriptions, and one-time purchases through Stripe. Instead of building your own billing system, you define products in the dashboard and Stack Auth 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

1

Enable Payments

Go to the Apps section in your dashboard, find Payments, and enable it.
2

Connect Stripe

Open Payments -> Settings and follow the Stripe Connect onboarding flow. You’ll be asked for business details, bank info, and identity verification. Once approved, payments are live.
3

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.
Stack Auth Payments is currently only available for US-based businesses. Support for other countries is coming soon.

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: 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 Stripe
  3. The user pays, Stripe notifies Stack Auth, 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). 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.
  • Include-by-default - Instead of a price, make the product free and automatically available to every customer. Useful for free-tier plans.
  • 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.
app/components/purchase-button.tsx
"use client";
import { useUser } from "@stackframe/stack";

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>;
}
For team purchases, call createCheckoutUrl on the team object instead:
const team = user.useTeam(teamId);
const checkoutUrl = await team.createCheckoutUrl({ productId });
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 for details.

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

// 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.
// 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:
app/components/credits-widget.tsx
"use client";
import { useUser } from "@stackframe/stack";

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.
lib/credits.ts
import { stackServerApp } from "@/stack/server";

export async function consumeCredits(userId: string, amount: number) {
  const user = await stackServerApp.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 };
}
Always use tryDecreaseQuantity() instead of checking the balance and then decreasing. This prevents race conditions where multiple requests could consume more credits than available.
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:
app/components/upgrade-button.tsx
"use client";
import { useUser } from "@stackframe/stack";

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>
  );
}
Switching plans requires the customer to have a default payment method saved. The switch happens immediately and Stripe prorates the charge automatically.

Canceling a subscription

cancelSubscription is called on the app instance rather than the user object:
"use client";
import { useStackApp } from "@stackframe/stack";

export default function CancelButton({ productId }: { productId: string }) {
  const app = useStackApp();
  return (
    <button onClick={async () => {
      await app.cancelSubscription({ productId });
    }}>
      Cancel Subscription
    </button>
  );
}

Billing and payment methods

Customers can save a payment method for future purchases and plan switches. This is built on Stripe’s SetupIntents.
// Create a setup intent
const setupIntent = await user.createPaymentMethodSetupIntent();
// setupIntent.clientSecret - use with Stripe Elements to collect card details
// setupIntent.stripeAccountId - the connected Stripe account ID

// After the user completes Stripe's 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:
// Client component (hook)
const billing = user.useBilling();

// Server component
const billing = await user.getBilling();

// billing.hasCustomer - whether a Stripe customer exists
// billing.defaultPaymentMethod - card details or null

Invoices

List a customer’s invoices for displaying billing history:
// 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 Stripe’s hosted invoice page). Invoices support pagination:
const firstPage = await user.listInvoices({ limit: 10 });
const secondPage = await user.listInvoices({
  limit: 10,
  cursor: firstPage.nextCursor,
});
Invoices are available for user and team customers only, not custom customers.

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:
// Grant a pre-configured product to a user
await stackServerApp.grantProduct({
  userId: "user-id",
  productId: "prod_premium",
});

// Grant to a team
await stackServerApp.grantProduct({
  teamId: "team-id",
  productId: "prod_team_plan",
});

// Grant to a custom customer
await stackServerApp.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:
await stackServerApp.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

Stack Auth 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 stackServerApp 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 Stripe, 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 Stripe. When you’re ready to test with real Stripe flows (but still using test credentials), disable Stack’s test mode and use Stripe’s test card numbers:
  • Success: 4242 4242 4242 4242
  • Decline: 4000 0000 0000 0002
  • Insufficient Funds: 4000 0000 0000 9995
See Stripe’s testing documentation for more test scenarios.