Stripe Integration
Stripe Integration Guide
This guide walks you through setting up a complete Stripe integration with QZPay.
Prerequisites
- Stripe account with API keys
- Node.js 22+
- PostgreSQL database
Setup
-
Install packages
Terminal window pnpm add @qazuor/qzpay-core @qazuor/qzpay-stripe @qazuor/qzpay-drizzle stripe drizzle-orm postgres -
Configure environment variables
.env DATABASE_URL=postgresql://user:pass@localhost:5432/billingSTRIPE_SECRET_KEY=sk_test_xxxSTRIPE_PUBLISHABLE_KEY=pk_test_xxxSTRIPE_WEBHOOK_SECRET=whsec_xxx -
Set up database
src/db/schema.ts export * from '@qazuor/qzpay-drizzle/schema';Terminal window pnpm drizzle-kit push -
Initialize billing service
src/billing.ts import { createQZPayBilling } from '@qazuor/qzpay-core';import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';import { createQZPayDrizzleAdapter } from '@qazuor/qzpay-drizzle';import { drizzle } from 'drizzle-orm/postgres-js';import postgres from 'postgres';const sql = postgres(process.env.DATABASE_URL!);const db = drizzle(sql);export const billing = createQZPayBilling({paymentAdapter: createQZPayStripeAdapter({secretKey: process.env.STRIPE_SECRET_KEY!}),storage: createQZPayDrizzleAdapter(db)});
Customer Management
Creating Customers
When a user signs up, create a QZPay customer:
export async function createBillingCustomer(user: User) { const customer = await billing.customers.create({ email: user.email, name: user.name, metadata: { userId: user.id, registeredAt: new Date().toISOString() } });
// Store customer ID in your user record await updateUser(user.id, { customerId: customer.id });
return customer;}Syncing with Stripe
QZPay automatically syncs customers with Stripe:
const customer = await billing.customers.create({ email: 'user@example.com' });
// The customer is created in both QZPay storage AND Stripeconsole.log(customer.externalIds.stripe); // 'cus_xxx'Subscription Flow
1. Display Plans
// Fetch plans from Stripeconst plans = await billing.plans.list();
// Return to frontendreturn plans.map(plan => ({ id: plan.id, name: plan.name, price: plan.prices[0].amount, interval: plan.prices[0].interval}));2. Create Checkout Session
Use the payment adapter to create checkout sessions:
import { createQZPayStripeAdapter } from '@qazuor/qzpay-stripe';
const stripeAdapter = createQZPayStripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY!});
export async function createCheckoutSession( customerId: string, priceId: string) { // Get customer's provider ID const customer = await billing.customers.get(customerId); const providerCustomerId = customer?.externalIds?.stripe;
const session = await stripeAdapter.checkout.create({ customerId: providerCustomerId, mode: 'subscription', lineItems: [{ priceId, quantity: 1 }], successUrl: `${APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`, cancelUrl: `${APP_URL}/pricing` }, [priceId]); // Provider price IDs
return { url: session.url };}3. Handle Success
// After successful checkout, Stripe redirects to successUrl// The webhook will handle the actual subscription creation
export async function handleCheckoutSuccess(sessionId: string) { // Retrieve the checkout session from Stripe const session = await stripeAdapter.checkout.retrieve(sessionId);
// The subscription is already created via webhook // Find the subscription by provider ID const subscription = await billing.subscriptions.getByExternalId( session.subscriptionId! );
return subscription;}Webhook Handling
Setting Up Webhooks
-
Create webhook endpoint
src/routes/webhooks.ts import { Hono } from 'hono';import { createWebhookRouter } from '@qazuor/qzpay-hono';import { billing, stripeAdapter } from '../billing';// Create webhook router with Hono integrationconst webhookRouter = createWebhookRouter({billing,paymentAdapter: stripeAdapter,handlers: {'customer.subscription.created': async (c, event) => {console.log('New subscription:', event.data);},'invoice.payment_succeeded': async (c, event) => {console.log('Payment succeeded:', event.data);}},onEvent: async (c, event) => {// Called for all eventsconsole.log('Webhook event:', event.type);}});const app = new Hono();app.route('/stripe', webhookRouter);export default app; -
Configure in Stripe Dashboard
Go to Developers → Webhooks → Add endpoint:
- URL:
https://yourdomain.com/webhooks/stripe - Events to listen for:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.payment_failed
- URL:
-
Test locally with Stripe CLI
Terminal window stripe listen --forward-to localhost:3000/webhooks/stripe
Custom Event Handlers
// React to billing eventsbilling.on('subscription.created', async (event) => { const { customerId, planId } = event.data;
// Provision features await provisionFeatures(customerId, planId);
// Send welcome email await sendEmail(customerId, 'welcome_to_plan', { planId });});
billing.on('subscription.canceled', async (event) => { // Revoke features at period end await scheduleFeatureRevocation( event.data.customerId, event.data.currentPeriodEnd );
// Send feedback survey await sendCancellationSurvey(event.data.customerId);});
billing.on('payment.failed', async (event) => { // Notify customer await sendPaymentFailedEmail(event.data.customerId);
// Internal alert await alertSlack(`Payment failed: ${event.data.id}`);});Managing Subscriptions
Changing Plans
export async function changePlan( subscriptionId: string, newPriceId: string) { return billing.subscriptions.update(subscriptionId, { planId: newPriceId, proration: 'create_prorations' // or 'none' });}Cancellation
export async function cancelSubscription( subscriptionId: string, feedback?: string) { const subscription = await billing.subscriptions.cancel(subscriptionId, { atPeriodEnd: true // User keeps access until period ends });
// Store cancellation feedback if (feedback) { await storeFeedback(subscriptionId, feedback); }
return subscription;}Customer Portal
Enable Stripe’s Customer Portal for self-service:
export async function createPortalSession(customerId: string) { const customer = await billing.customers.get(customerId); const stripeCustomerId = customer.externalIds.stripe;
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const session = await stripe.billingPortal.sessions.create({ customer: stripeCustomerId, return_url: `${APP_URL}/settings/billing` });
return { url: session.url };}Testing
Use Stripe test mode for development:
# Test API keysSTRIPE_SECRET_KEY=sk_test_xxxSTRIPE_PUBLISHABLE_KEY=pk_test_xxxTest card numbers:
| Card | Scenario |
|---|---|
4242424242424242 | Success |
4000000000000002 | Declined |
4000002500003155 | Requires 3DS |
4000000000009995 | Insufficient funds |