Use when implementing payment processing in Stacks — Stripe charges, subscriptions, checkout sessions, customer management, payment methods, invoices, coupons, promo codes, products, prices, webhooks, or the Payment facade. Covers @stacksjs/payments and config/payment.ts.
Full Stripe integration via the Payment facade. Uses Stripe API version 2026-01-28.clover. The Stripe SDK is initialized from services.stripe.secretKey (sourced from config/payment.ts).
storage/framework/core/payments/src/storage/framework/core/payments/src/payment.tsstorage/framework/core/payments/src/drivers/stripe.tsstorage/framework/core/payments/src/billable/config/payment.tsconfig/saas.tsstorage/framework/defaults/functions/billing/payments.tsPayment (default) -- facade object with all payment methodscharge, subscribe, checkout, etc.)manageCharge, manageCustomer, manageSubscription, etc.)stripe -- raw Stripe SDK instanceStripe -- re-exported Stripe types namespaceThe Payment object aggregates all payment operations. Every method is also available as a standalone export.
import { Payment } from '@stacksjs/payments'
// Create and confirm a charge (creates PaymentIntent with confirm: true)
const intent = await Payment.charge(user, 2999, 'pm_xxx', { currency: 'usd' })
// Create PaymentIntent without confirming (for client-side confirmation)
const intent = await Payment.createPayment(user, 2999, { currency: 'usd' })
// Refund -- partial
const refund = await Payment.refund('pi_xxx', 1000)
// Refund -- full (omit amount)
const refund = await Payment.refund('pi_xxx')
charge() sets confirmation_method: 'automatic', confirm: true, attaches the payment method, and delegates to createPayment(). If the user has a stripe_id, it is set as the customer on the PaymentIntent. Default currency is 'usd'.
The manageCharge module also exposes findPayment(id) which retrieves a PaymentIntent by ID, returning null on failure.
// One-time payment checkout
const session = await Payment.checkout(user, [
{ price: 'price_xxx', quantity: 1 }
], { success_url: '/success', cancel_url: '/cancel' })
// Subscription checkout
const subSession = await Payment.subscriptionCheckout(user, 'price_xxx', {
success_url: '/success', cancel_url: '/cancel'
})
Both methods require the user to have a stripe_id (throws if missing). checkout() sets mode: 'payment', subscriptionCheckout() sets mode: 'subscription'.
// Create -- uses lookup_key to resolve the Stripe Price
const sub = await Payment.subscribe(user, 'premium-monthly')
// Cancel at period end (prorate: true)
const cancelled = await Payment.cancelSubscription('sub_xxx')
// Cancel immediately (invoice_now: true, prorate: false)
const cancelled = await Payment.cancelSubscription('sub_xxx', true)
// Check active subscription
const hasActive = await Payment.hasActiveSubscription(user, 'default')
// Change plan -- swaps the subscription item's price
const changed = await Payment.changeSubscription(user, 'enterprise-monthly')
subscribe() calls managePrice.retrieveByLookupKey(lookupKey) to find the price, then creates the subscription with payment_behavior: 'allow_incomplete' and expand: ['latest_invoice.payment_intent']. It stores the subscription in the subscriptions database table.
isValid() returns true if the subscription status is 'active' or 'trialing'. isIncomplete() checks for 'incomplete' status.
cancel() calls stripe.subscriptions.cancel() and updates provider_status to 'canceled' in the database.
update() retrieves the active subscription via user.activeSubscription(), finds the new price by lookup key, updates the subscription item, and stores the updated price in the database.
// Get existing or create new Stripe customer
const customer = await Payment.getOrCreateCustomer(user, { name: 'John' })
// Update customer details in Stripe
const updated = await Payment.updateCustomer(user, { name: 'Jane' })
// Delete from Stripe and clear stripe_id on user model
const deleted = await Payment.deleteCustomer(user)
createOrGetStripeUser() checks user.stripe_id first. If the user has one, it retrieves the customer from Stripe. If the customer was deleted (404 or deleted: true), it creates a new one. On creation, it auto-fills name and email from the user model and calls user.update({ stripe_id: customer.id }).
Additional methods on manageCustomer:
stripeId(user) -- returns user.stripe_idhasStripeId(user) -- boolean checkcreateStripeCustomer(user, options) -- throws if user already has a stripe_idcreateOrUpdateStripeUser(user, options) -- creates or updatesretrieveStripeUser(user) -- returns customer or undefinedsyncStripeCustomerDetails(user, options) -- updates Stripe with user's name, email, address, locales, metadata// Add a payment method to the customer
const pm = await Payment.addPaymentMethod(user, 'pm_xxx')
// Set as default (by Stripe payment method ID string)
const customer = await Payment.setDefaultPaymentMethod(user, 'pm_xxx')
// Remove a payment method (by database record ID number)
const removed = await Payment.removePaymentMethod(user, paymentMethodDbId)
// Create a setup intent for collecting payment methods
const intent = await Payment.createSetupIntent(user, { payment_method_types: ['card'] })
addPaymentMethod() accepts a string (Stripe PM ID) or Stripe.PaymentMethod object. It attaches the PM to the customer if not already attached, then stores it in the payment_methods table with type, last_four, brand, exp_year, exp_month, user_id, provider_id.
setUserDefaultPayment() accepts a Stripe PM ID string, clears existing is_default flags, sets the new default in the database, and updates invoice_settings.default_payment_method on the Stripe customer.
setDefaultPaymentMethod() accepts a database record ID number.
Additional methods on managePaymentMethod:
updatePaymentMethod(user, pmId, params) -- updates PM in StripelistPaymentMethods(user) -- queries payment_methods table by user_idretrievePaymentMethod(user, pmId) -- by database IDretrieveDefaultPaymentMethod(user) -- finds where is_default: trueconst invoices = await Payment.getInvoices(user) // lists with expanded payment_intent.payment_method
const invoice = await Payment.createInvoice('cus_xxx', { description: 'Custom invoice' })
const paid = await Payment.payInvoice('inv_xxx')
getInvoices() requires the user to have a stripe_id and expands data.payment_intent.payment_method.
// Create a product with its price
const { product, price } = await Payment.createProduct('Pro Plan', 2999, {
currency: 'usd', interval: 'month', description: 'Pro features'
})
// Get a price by Stripe lookup_key
const price = await Payment.getPrice('premium-monthly')
// List active products
const products = await Payment.listProducts({ limit: 10 })
createProduct() creates the Stripe product, then creates a price linked to it. If interval is provided, the price gets recurring: { interval }.
Additional methods on manageProduct:
create(params), retrieve(productId), update(productId, params)archive(productId) -- sets active: falsesearch(query, params) -- Stripe product searchlist(params) -- defaults to active: trueAdditional methods on managePriceExtended:
create(params), retrieve(priceId), update(priceId, params)list(params) -- defaults to active: truelistByProduct(productId, params) -- prices for a specific productsearch(query, params) -- Stripe price searcharchive(priceId) -- sets active: falseconst coupon = await Payment.createCoupon({
percentOff: 20, duration: 'once', name: 'Holiday Sale', maxRedemptions: 100
})
// Or amount-based: { amountOff: 500, currency: 'usd', duration: 'forever' }
const promo = await Payment.createPromoCode('coupon_xxx', 'SAVE20')
const valid = await Payment.validatePromoCode('SAVE20') // returns PromotionCode | null
createCoupon() supports percentOff, amountOff, currency, duration ('forever' | 'once' | 'repeating'), durationInMonths, name, maxRedemptions.
Additional methods on manageCoupon:
retrieve(couponId), update(couponId, params), delete(couponId), list(params)createPromotionCode(params) -- wraps stripe.promotionCodes.create()retrievePromotionCode(code) -- lists active promotion codes matching the code stringimport { manageTransaction } from '@stacksjs/payments'
const tx = await manageTransaction.store(user, productId, {
brand: 'visa', provider_id: 'pi_xxx', description: 'Purchase', type: 'one-time'
})
const txList = await manageTransaction.list(user)
Transactions are stored in the payment_transactions table with name (from product), amount (from product unit_price), brand, type, provider_id, user_id.
import { Payment } from '@stacksjs/payments'
// Register handlers
Payment.onPaymentIntent({
succeeded: async (event) => { /* ... */ },
failed: async (event) => { /* ... */ },
created: async (event) => { /* ... */ },
canceled: async (event) => { /* ... */ },
})
Payment.onSubscription({
created: async (event) => { /* ... */ },
updated: async (event) => { /* ... */ },
deleted: async (event) => { /* ... */ },
trialWillEnd: async (event) => { /* ... */ },
})
Payment.onInvoice({
paid: async (event) => { /* ... */ },
paymentFailed: async (event) => { /* ... */ },
created: async (event) => { /* ... */ },
finalized: async (event) => { /* ... */ },
})
Payment.onCheckout({
completed: async (event) => { /* ... */ },
expired: async (event) => { /* ... */ },
})
Payment.onCharge({
succeeded: async (event) => { /* ... */ },
failed: async (event) => { /* ... */ },
refunded: async (event) => { /* ... */ },
disputed: async (event) => { /* ... */ },
})
// Process incoming webhook request
const result = await Payment.processWebhook(rawPayload, signatureHeader, {
secret: 'whsec_xxx', tolerance: 300
})
// Returns: { success: boolean, eventType?: string, error?: string }
// Low-level: register any event type
Payment.webhook.onWebhookEvent('payment_method.attached', handler)
Payment.webhook.registerWebhookHandlers({ 'payment_intent.succeeded': handler })
// Event data extractors
import { getPaymentIntent, getSubscription, getInvoice, getCheckoutSession, getCharge, getCustomer } from '@stacksjs/payments'
Supported webhook event types: payment_intent.succeeded, payment_intent.payment_failed, payment_intent.created, payment_intent.canceled, customer.subscription.created/updated/deleted, customer.subscription.trial_will_end, customer.created/updated/deleted, invoice.paid/payment_failed/finalized/created, checkout.session.completed/expired, charge.succeeded/failed/refunded, charge.dispute.created/closed, payment_method.attached/detached, setup_intent.succeeded/setup_failed, plus any string.
import { createStripeProduct } from '@stacksjs/payments'
const result = await createStripeProduct()
// Creates Stripe products and prices from config/saas.ts plans
This iterates saas.plans, creates a Stripe product per plan, then creates prices for each pricing option using lookup_key from the key field.
Payment.formatAmount(2999, 'usd') // '$29.99' (uses Intl.NumberFormat)
Payment.toCents(29.99) // 2999
Payment.toDollars(2999) // 29.99
The storage/framework/defaults/functions/billing/payments.ts file provides a useBillable() composable for frontend billing UI:
const { loadCardForm, loadPaymentForm, handleAddPaymentMethod, handlePayment } = useBillable()
await loadCardForm(clientSecret) // loads Stripe Card Element
await loadPaymentForm(clientSecret) // loads Stripe Payment Element
await handleAddPaymentMethod(clientSecret, elements) // confirms card setup
await handlePayment(elements) // confirms payment
{
driver: 'stripe',
stripe: {
publishableKey: env.STRIPE_PUBLISHABLE_KEY || '',
secretKey: env.STRIPE_SECRET_KEY || '',
},
} satisfies PaymentConfig
Plans use productName, description, metadata, and a pricing array where each entry has key (lookup_key), price (in cents), interval (optional: 'month' | 'year'), currency:
{
plans: [
{
productName: 'Stacks Hobby',
description: 'All the Stacks features.',
pricing: [
{ key: 'stacks_hobby_early_monthly', price: 1900, interval: 'month', currency: 'usd' },
{ key: 'stacks_hobby_yearly', price: 37900, interval: 'year', currency: 'usd' },
],
metadata: { createdBy: 'admin', version: '1.0.0' },
},
// ... more plans
],
webhook: { endpoint: 'your-webhook-endpoint', secret: 'your-webhook-secret' },
currencies: ['usd'],
coupons: [],
products: [
{ name: 'Stacks Hobby', description: '...', images: ['image-url'] },
],
} satisfies SaasConfig
subscriptions -- columns: user_id, type, unit_price, provider_id, provider_status, provider_price_id, quantity, trial_ends_at, ends_at, provider_type, last_used_atpayment_methods -- columns: id, type, last_four, brand, exp_year, exp_month, user_id, provider_id, is_defaultpayment_products -- columns: id, name, unit_pricepayment_transactions -- columns: id, name, description, amount, brand, type, provider_id, user_idThe UserModel must have:
id, name, email, stripe_id fieldshasStripeId() method -- returns booleanupdate(data) method -- for persisting stripe_idactiveSubscription() method -- for subscription updates.env as STRIPE_SECRET_KEY and STRIPE_PUBLISHABLE_KEY -- never hardcode them in config filesSTRIPE_SECRET_KEY is missing, the module throws on importtoCents() and toDollars() for conversioncharge() creates AND confirms the PaymentIntent in one stepsubscribe() resolves the price via lookup_key, not a direct Stripe price IDremovePaymentMethod() takes a database record ID (number), not a Stripe PM ID (string)setDefaultPaymentMethod has two variants: one takes a Stripe PM ID string (setUserDefaultPayment), the other takes a database ID numbergetOrCreateCustomer() handles deleted Stripe customers by recreating themlist() on products defaults to active: true onlyprocessWebhook() uses stripe.webhooks.constructEvent() for signature verification