Billing & subscriptions
createBilling wraps the Stripe API (checkout, portal, subscriptions) with no SDK — just fetch — plus webhook signature verification, the critical piece everyone gets wrong. Here the Stripe transport is mocked (no keys needed); the subscription state is really persisted via the ORM.
Tip
The HTTP transport is injectable: pass secretKey for real Stripe, or a mock for tests. constructEvent() reconstructs ${t}.${payload}, hex-HMAC-SHA256s it, constant-time compares to the header's v1, and enforces a replay window — the webhook protection Cashier gives you.
Status
✓ subscribed — plan Pro
Webhook signature verification
Stripe-Signature (t, v1)valid signature: accepted ✓Stripe-Signature (altered v1)tampered signature: rejected ✗import { createBilling, subscribed, constructEvent } from "@bext-stack/framework/billing";
const billing = createBilling({ secretKey: process.env.STRIPE_SECRET_KEY });
// start a subscription checkout:
const { url } = await billing.checkoutSession({
priceId: "price_pro", mode: "subscription",
successUrl: "/ok", cancelUrl: "/cancel", customer: "cus_1",
}); // redirect the user to `url`
// gate features on an active subscription:
const subs = await billing.listSubscriptions({ customer: "cus_1" });
if (subscribed(subs)) { /* unlock */ }
// webhook route — verify the signature (constant-time HMAC + replay window):
export async function POST({ request }) {
const body = await request.text();
const event = constructEvent(body, request.headers.get("stripe-signature"), process.env.STRIPE_WEBHOOK_SECRET);
if (!event) return new Response("bad signature", { status: 400 });
if (event.type === "checkout.session.completed") { /* mark active */ }
return new Response(null, { status: 200 });
}