Skip to main content
A smooth checkout experience directly impacts your conversion rate. BillingOS gives you a PCI-compliant checkout modal that handles card collection, 3D Secure, and subscription creation — all without touching Stripe directly.

Two ways to accept payments

ApproachBest forEffort
Checkout modal (recommended)Most apps — drop-in, PCI-compliant, handles everything3 lines of code
Checkout APICustom flows where you need full controlMore code, more flexibility

Checkout modal

The CheckoutModal renders a secure iframe-based checkout. Card data never touches your servers.

With PricingTable (easiest)

The PricingTable component opens the checkout modal automatically when a user selects a plan — no extra code needed:
<PricingTable
  onPlanChanged={(subscription) => {
    console.log("Subscribed!", subscription);
  }}
/>
See Show a pricing page for full customization options.

Standalone checkout modal

For custom UIs where you want to trigger checkout yourself:
"use client";

import { useState } from "react";
import { CheckoutModal } from "@billingos/sdk";

export default function UpgradeButton() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Upgrade to Pro
      </button>

      <CheckoutModal
        open={open}
        onOpenChange={setOpen}
        priceId="price_pro_monthly"
        onSuccess={(subscription) => {
          console.log("Payment successful!", subscription);
          setOpen(false);
        }}
      />
    </>
  );
}
Checkout modal

Prefill customer information

Skip the email step by passing customer data:
<CheckoutModal
  open={open}
  onOpenChange={setOpen}
  priceId="price_pro_monthly"
  customer={{
    email: "user@example.com",
    name: "Jane Smith",
  }}
  onSuccess={handleSuccess}
/>

Handle events

<CheckoutModal
  open={open}
  onOpenChange={setOpen}
  priceId="price_pro_monthly"
  onSuccess={(subscription) => {
    toast.success("Welcome to Pro!");
    router.push("/dashboard");
  }}
  onError={(error) => {
    toast.error(`Payment failed: ${error.message}`);
  }}
  onCancel={() => {
    console.log("User closed checkout");
  }}
/>

Adaptive pricing

Enable localized pricing based on the customer’s location:
<CheckoutModal
  open={open}
  onOpenChange={setOpen}
  priceId="price_pro_monthly"
  adaptivePricing={true}
  onSuccess={handleSuccess}
/>
The checkout modal runs in a secure iframe. Card details are handled entirely by Stripe — your app never processes or stores payment information. This means you’re PCI-compliant by default.

Checkout API

For fully custom checkout flows, use the useCheckout hook:
"use client";

import { useCheckout } from "@billingos/sdk";

export default function CustomCheckout() {
  const { openCheckout, isLoading, error } = useCheckout();

  const handleUpgrade = async () => {
    await openCheckout({
      priceId: "price_pro_monthly",
      customer: {
        email: "user@example.com",
      },
    });
  };

  return (
    <button onClick={handleUpgrade} disabled={isLoading}>
      {isLoading ? "Processing..." : "Upgrade to Pro"}
    </button>
  );
}

Imperative API

You can also open checkout programmatically from anywhere in your app:
window.billingOS?.checkout.open({
  priceId: "price_pro_monthly",
  onSuccess: (subscription) => {
    console.log("Subscribed!", subscription);
  },
});

Testing payments

Use these test card numbers in development:
CardResult
4242 4242 4242 4242Successful payment
4000 0000 0000 32203D Secure authentication
4000 0000 0000 0002Card declined
Use any future expiry date and any 3-digit CVC.

What happens after payment

  1. BillingOS creates a Stripe subscription
  2. The onSuccess callback fires with the subscription object
  3. The customer’s entitlements are immediately updated
  4. Feature gates (useFeature, <FeatureGate>) reflect the new plan
  5. The PricingTable shows “Current Plan” on the subscribed plan