startStripeCheckout()
Creates a Stripe Checkout Session for the given user and lineItems. Ensures a Stripe Customer exists first, then returns the hosted checkoutUrl.
Usage - Driver
import { startCheckout } from '@payments/driver'If you have ‘stripe’ set up as the default payment driver in
@app/configofcourse.
const { checkoutUrl } = await startCheckout({
user,
lineItems: [{ priceVariantId: 'price_XXXX', quantity: 1 }],
mode: 'subscription',
successUrl: `${baseURL}/billing/success`,
cancelUrl: `${baseURL}/billing/cancel`,
trialDays: 14,
locale: 'auto',
autoCalculateTax: true,
coupons: ['WELCOME10'],
})
// redirect(checkoutUrl)Usage - Directly
import { startStripeCheckout } from '@payments/stripe/resolvers/startStripeCheckout.resolver'const { checkoutUrl } = await startStripeCheckout({
user,
lineItems: [{ priceVariantId: 'price_XXXX', quantity: 1 }],
mode: 'subscription',
successUrl: `${baseURL}/billing/success`,
cancelUrl: `${baseURL}/billing/cancel`,
trialDays: 14,
locale: 'auto',
autoCalculateTax: true,
coupons: ['WELCOME10'],
})
// redirect(checkoutUrl)Implementation details
- Calls
ensureStripeCustomer()to get{ user, customer }. - Maps
lineItemsto Stripeline_itemsformat and merges coupon/promo codes. - Enables automatic tax collection when requested.
- Optionally sets locale and / or subscription trial days if passed.
- Returns the hosted session URL for you to redirect to.
startStripeCheckout.resolver.ts
export const startStripeCheckout = createResolver(async ({ args, parseArgs, formatOutput }) => {
// Args
const { mode, lineItems, successUrl, cancelUrl, metadata, customerOverrides } = parseArgs(args)
// Ensure there is a customer first
const { user, customer } = await ensureStripeCustomer({ user: args.user, metadata, customerOverrides })
// Normalize lineItems & discounts from driver format to stripe format
const stripeLineItems = lineItems.map(item => ({
price: item.priceVariantId,
quantity: item.quantity || 1,
}))
const stripeDiscounts = [
...(args.coupons ? args.coupons.map(code => ({ coupon: code })) : []),
...(args.promoCodes ? args.promoCodes.map(code => ({ promotion_code: code })) : []),
]
// Start session
const session = await createCustomerCheckoutSession({
// Recommended: Always pass a customerId (why we call ensureCustomer() first)
customerId: customer.customerId,
customer: customer.customerId,
// Main config
mode, // e.g. 'subsription' | 'checkout'
lineItems: stripeLineItems,
discounts: stripeDiscounts,
// language & taxes?
locale: args.locale || 'auto',
automatic_tax: { enabled: args.autoCalculateTax || false },
// trial for subscriptions?
...((args.trialDays && mode === 'subscription') ? { subscription_data: { trial_period_days: args.trialDays } } : {}),
// Recommended: Always add at least userId here
metadata: { userId: user.userId, ...(metadata || {}) },
// Recommended:
successUrl,
cancelUrl,
})
return formatOutput({ provider: 'stripe', user, customer, checkoutUrl: session.url! })
})StartCheckoutInput
Follows the driver signature StartCheckoutInput schema:
import { StartCheckoutInput } from '@payments/driver/driver.signature'export const StartCheckoutInput = schema('StartCheckoutInput', {
user: User,
lineItems: CheckoutLineItem.array().describe('Array of line items to include in the checkout session'),
mode: z.enum(['payment', 'subscription', 'setup']).describe('Whether this is a one-time purchase or a recurring subscription'),
successUrl: z.string().url().describe('URL to redirect the user to after successful checkout'),
cancelUrl: z.string().url().describe('URL to redirect the user to if they cancel the checkout'),
coupons: z.array(z.string()).nullish().describe('Array of coupon codes to apply to the checkout session'),
promoCodes: z.array(z.string()).nullish().describe('Array of promotion codes to apply to the checkout session'),
trialDays: z.number().int().min(0).nullish().describe('Number of trial days to apply for subscription checkouts'),
locale: z.string().nullish().describe('Locale/language code for the checkout session'),
autoCalculateTax: z.boolean().nullish().describe('Whether to automatically calculate tax for the checkout session'),
customerOverrides: z.object({
email: Customer.shape.email.nullish().describe('Override the email on the Customer record for this checkout session'),
name: Customer.shape.name.nullish().describe('Override the name on the Customer record for this checkout session'),
address: z.object({
line1: z.string().nullish().describe('Address line 1'),
city: z.string().nullish().describe('City'),
state: z.string().nullish().describe('State/Province'),
postalCode: z.string().nullish().describe('Postal/ZIP code'),
country: z.string().nullish().describe('Country code (ISO 3166-1 alpha-2)'),
}).nullish().describe('Override the address on the Customer record for this checkout session'),
taxId: z.string().nullish().describe('Override the tax ID on the Customer record for this checkout session'),
}).nullish().describe('Object containing fields to override on the Customer record for this checkout session'),
metadata: z.record(z.string(), z.any()).nullish().describe('Optional metadata to attach to the checkout session'),
})StartCheckoutOutput
Follows the driver signature StartCheckoutOutput schema:
import { StartCheckoutOutput } from '@payments/driver/driver.signature'export const StartCheckoutOutput = schema('StartCheckoutOutput', {
provider: PaymentProvidable.shape.provider,
user: User,
customer: Customer,
checkoutUrl: z.string().url().describe('URL of the checkout session to redirect the user to'),
})Recommendations
💡
It’s probably best to validate entitlement constraints (e.g., an existing subscription) before starting a new checkout. Should users be able to checkout multiple times? Is every item purchaseable multiple times? Or is there a limit where it makes more sense to redirect to either the dashboard or customer portal?
