GuidesStripe Integration
Checkout Sessions
Create checkout flows and handle payments
Checkout Sessions
Creating Checkout Session
import { action } from "./_generated/server";
import { v } from "convex/values";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
});
export const createCheckout = action({
args: {
priceId: v.string(),
organizationId: v.optional(v.id("organizations")),
},
handler: async (ctx, args) => {
const user = await ctx.auth.getUserIdentity();
if (!user) throw new Error("Unauthorized");
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [
{
price: args.priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
customer_email: user.email,
metadata: {
userId: user.subject,
organizationId: args.organizationId,
},
});
return session.url;
},
});Client-Side Integration
'use client';
import { useAction } from 'convex/react';
import { api } from '@/convex/_generated/api';
export function CheckoutButton({ priceId }: { priceId: string }) {
const createCheckout = useAction(api.stripe.createCheckout);
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
const url = await createCheckout({ priceId });
window.location.href = url;
} catch (error) {
console.error('Checkout failed:', error);
} finally {
setLoading(false);
}
};
return (
<button onClick={handleCheckout} disabled={loading}>
{loading ? 'Loading...' : 'Subscribe Now'}
</button>
);
}One-Time Payments
const session = await stripe.checkout.sessions.create({
mode: 'payment', // One-time payment
line_items: [
{
price: args.priceId,
quantity: 1,
},
],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
});Success Page
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
export default function DashboardPage() {
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
useEffect(() => {
if (sessionId) {
// Show success message
toast.success('Subscription activated!');
// Clean URL
window.history.replaceState({}, '', '/dashboard');
}
}, [sessionId]);
return <div>Dashboard</div>;
}Customization Options
Payment Methods
payment_method_types: ['card', 'us_bank_account', 'cashapp'],Billing Address Collection
billing_address_collection: 'required',Discount Codes
allow_promotion_codes: true,Trial Period
subscription_data: {
trial_period_days: 14,
},Tax Collection
automatic_tax: {
enabled: true,
},
tax_id_collection: {
enabled: true,
},Complete Example
export const createCheckout = action({
args: {
priceId: v.string(),
organizationId: v.optional(v.id("organizations")),
coupon: v.optional(v.string()),
trialDays: v.optional(v.number()),
},
handler: async (ctx, args) => {
const user = await ctx.auth.getUserIdentity();
if (!user) throw new Error("Unauthorized");
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
// Line Items
line_items: [{
price: args.priceId,
quantity: 1,
}],
// Customer
customer_email: user.email,
// URLs
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
// Features
allow_promotion_codes: true,
billing_address_collection: 'required',
payment_method_types: ['card'],
// Tax
automatic_tax: { enabled: true },
// Trial
subscription_data: args.trialDays ? {
trial_period_days: args.trialDays,
} : undefined,
// Discount
discounts: args.coupon ? [{ coupon: args.coupon }] : undefined,
// Metadata
metadata: {
userId: user.subject,
organizationId: args.organizationId,
},
});
return session.url;
},
});Error Handling
const handleCheckout = async () => {
setLoading(true);
setError(null);
try {
const url = await createCheckout({ priceId });
if (!url) throw new Error('Failed to create checkout session');
window.location.href = url;
} catch (err) {
setError(err.message);
toast.error('Checkout failed. Please try again.');
} finally {
setLoading(false);
}
};Testing
Use test card numbers:
Success: 4242 4242 4242 4242
Decline: 4000 0000 0000 0002
3D Secure: 4000 0027 6000 3184