Invoice Management Patterns

Generate, retrieve, and manage invoices with Stripe.

Overview#

Invoices provide billing history and receipts. This pattern covers:

  • Retrieving invoice history
  • Invoice webhooks
  • Creating custom invoices
  • Displaying invoices to users
  • Handling failed payments

Prerequisites#

npm install stripe

Get Invoice History#

Retrieve a customer's invoices from Stripe.

1// lib/billing.ts 2import { stripe } from '@/lib/stripe' 3import { prisma } from '@/lib/db' 4 5interface Invoice { 6 id: string 7 number: string | null 8 status: string | null 9 amount: number 10 currency: string 11 created: Date 12 dueDate: Date | null 13 paidAt: Date | null 14 pdfUrl: string | null 15 hostedUrl: string | null 16} 17 18export async function getInvoices(userId: string, limit = 10): Promise<Invoice[]> { 19 const user = await prisma.user.findUnique({ 20 where: { id: userId } 21 }) 22 23 if (!user?.stripeCustomerId) { 24 return [] 25 } 26 27 const invoices = await stripe.invoices.list({ 28 customer: user.stripeCustomerId, 29 limit, 30 expand: ['data.subscription'] 31 }) 32 33 return invoices.data.map(invoice => ({ 34 id: invoice.id, 35 number: invoice.number, 36 status: invoice.status, 37 amount: invoice.amount_due, 38 currency: invoice.currency, 39 created: new Date(invoice.created * 1000), 40 dueDate: invoice.due_date ? new Date(invoice.due_date * 1000) : null, 41 paidAt: invoice.status_transitions.paid_at 42 ? new Date(invoice.status_transitions.paid_at * 1000) 43 : null, 44 pdfUrl: invoice.invoice_pdf, 45 hostedUrl: invoice.hosted_invoice_url 46 })) 47}

Invoice API Route#

API endpoint to fetch invoices.

1// app/api/billing/invoices/route.ts 2import { auth } from '@/auth' 3import { getInvoices } from '@/lib/billing' 4import { NextResponse } from 'next/server' 5 6export async function GET(request: Request) { 7 const session = await auth() 8 9 if (!session?.user) { 10 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) 11 } 12 13 const { searchParams } = new URL(request.url) 14 const limit = parseInt(searchParams.get('limit') ?? '10') 15 16 try { 17 const invoices = await getInvoices(session.user.id, limit) 18 return NextResponse.json({ invoices }) 19 } catch (error) { 20 console.error('Failed to fetch invoices:', error) 21 return NextResponse.json( 22 { error: 'Failed to fetch invoices' }, 23 { status: 500 } 24 ) 25 } 26}

Invoice List Component#

Display invoice history to users.

1// components/billing/InvoiceList.tsx 2'use client' 3 4import { useState, useEffect } from 'react' 5import { FileText, Download, ExternalLink } from 'lucide-react' 6import { formatCurrency, formatDate } from '@/lib/utils' 7 8interface Invoice { 9 id: string 10 number: string | null 11 status: string | null 12 amount: number 13 currency: string 14 created: string 15 pdfUrl: string | null 16 hostedUrl: string | null 17} 18 19export function InvoiceList() { 20 const [invoices, setInvoices] = useState<Invoice[]>([]) 21 const [loading, setLoading] = useState(true) 22 23 useEffect(() => { 24 fetch('/api/billing/invoices') 25 .then(res => res.json()) 26 .then(data => setInvoices(data.invoices)) 27 .catch(console.error) 28 .finally(() => setLoading(false)) 29 }, []) 30 31 if (loading) { 32 return <InvoiceListSkeleton /> 33 } 34 35 if (invoices.length === 0) { 36 return ( 37 <div className="rounded-lg border p-8 text-center"> 38 <FileText className="mx-auto h-8 w-8 text-gray-400" /> 39 <p className="mt-2 text-gray-500">No invoices yet</p> 40 </div> 41 ) 42 } 43 44 return ( 45 <div className="divide-y rounded-lg border"> 46 {invoices.map(invoice => ( 47 <div key={invoice.id} className="flex items-center justify-between p-4"> 48 <div> 49 <p className="font-medium"> 50 Invoice {invoice.number ?? invoice.id.slice(0, 8)} 51 </p> 52 <p className="text-sm text-gray-500"> 53 {formatDate(new Date(invoice.created))} 54 </p> 55 </div> 56 57 <div className="flex items-center gap-4"> 58 <StatusBadge status={invoice.status} /> 59 60 <span className="font-medium"> 61 {formatCurrency(invoice.amount / 100, invoice.currency)} 62 </span> 63 64 <div className="flex gap-2"> 65 {invoice.pdfUrl && ( 66 <a 67 href={invoice.pdfUrl} 68 target="_blank" 69 rel="noopener noreferrer" 70 className="rounded p-2 hover:bg-gray-100" 71 title="Download PDF" 72 > 73 <Download className="h-4 w-4" /> 74 </a> 75 )} 76 {invoice.hostedUrl && ( 77 <a 78 href={invoice.hostedUrl} 79 target="_blank" 80 rel="noopener noreferrer" 81 className="rounded p-2 hover:bg-gray-100" 82 title="View Invoice" 83 > 84 <ExternalLink className="h-4 w-4" /> 85 </a> 86 )} 87 </div> 88 </div> 89 </div> 90 ))} 91 </div> 92 ) 93} 94 95function StatusBadge({ status }: { status: string | null }) { 96 const styles = { 97 paid: 'bg-green-100 text-green-800', 98 open: 'bg-amber-100 text-amber-800', 99 void: 'bg-gray-100 text-gray-800', 100 uncollectible: 'bg-red-100 text-red-800', 101 draft: 'bg-blue-100 text-blue-800' 102 }[status ?? 'draft'] ?? 'bg-gray-100 text-gray-800' 103 104 return ( 105 <span className={`rounded-full px-2 py-1 text-xs ${styles}`}> 106 {status ?? 'Unknown'} 107 </span> 108 ) 109}

Invoice Webhooks#

Handle invoice-related webhook events.

1// lib/webhook-handlers.ts 2import { stripe } from '@/lib/stripe' 3import { prisma } from '@/lib/db' 4import { sendEmail } from '@/lib/email' 5import Stripe from 'stripe' 6 7export async function handleInvoicePaid(invoice: Stripe.Invoice) { 8 const customerId = invoice.customer as string 9 10 const user = await prisma.user.findFirst({ 11 where: { stripeCustomerId: customerId } 12 }) 13 14 if (!user) return 15 16 // Store invoice record locally 17 await prisma.invoice.upsert({ 18 where: { stripeInvoiceId: invoice.id }, 19 create: { 20 userId: user.id, 21 stripeInvoiceId: invoice.id, 22 number: invoice.number, 23 amount: invoice.amount_paid, 24 currency: invoice.currency, 25 status: 'paid', 26 paidAt: new Date() 27 }, 28 update: { 29 status: 'paid', 30 paidAt: new Date() 31 } 32 }) 33 34 // Send receipt email 35 await sendEmail({ 36 to: user.email, 37 subject: `Receipt for Invoice ${invoice.number}`, 38 template: 'invoice-receipt', 39 data: { 40 invoiceNumber: invoice.number, 41 amount: invoice.amount_paid, 42 currency: invoice.currency, 43 pdfUrl: invoice.invoice_pdf 44 } 45 }) 46} 47 48export async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) { 49 const customerId = invoice.customer as string 50 51 const user = await prisma.user.findFirst({ 52 where: { stripeCustomerId: customerId } 53 }) 54 55 if (!user) return 56 57 // Update invoice status 58 await prisma.invoice.upsert({ 59 where: { stripeInvoiceId: invoice.id }, 60 create: { 61 userId: user.id, 62 stripeInvoiceId: invoice.id, 63 number: invoice.number, 64 amount: invoice.amount_due, 65 currency: invoice.currency, 66 status: 'failed' 67 }, 68 update: { 69 status: 'failed' 70 } 71 }) 72 73 // Send payment failed notification 74 await sendEmail({ 75 to: user.email, 76 subject: 'Payment Failed - Action Required', 77 template: 'payment-failed', 78 data: { 79 amount: invoice.amount_due, 80 currency: invoice.currency, 81 updateUrl: `${process.env.NEXT_PUBLIC_URL}/settings/billing` 82 } 83 }) 84} 85 86export async function handleUpcomingInvoice(invoice: Stripe.Invoice) { 87 const customerId = invoice.customer as string 88 89 const user = await prisma.user.findFirst({ 90 where: { stripeCustomerId: customerId } 91 }) 92 93 if (!user) return 94 95 // Send reminder for upcoming charge 96 await sendEmail({ 97 to: user.email, 98 subject: 'Upcoming Invoice', 99 template: 'invoice-upcoming', 100 data: { 101 amount: invoice.amount_due, 102 currency: invoice.currency, 103 date: new Date((invoice.next_payment_attempt ?? invoice.created) * 1000) 104 } 105 }) 106}

Create Custom Invoice#

Generate invoices for one-off charges.

1// lib/billing.ts 2export async function createCustomInvoice( 3 userId: string, 4 items: { description: string; amount: number }[] 5) { 6 const user = await prisma.user.findUnique({ 7 where: { id: userId } 8 }) 9 10 if (!user?.stripeCustomerId) { 11 throw new Error('No billing account') 12 } 13 14 // Create invoice items 15 for (const item of items) { 16 await stripe.invoiceItems.create({ 17 customer: user.stripeCustomerId, 18 amount: item.amount, // in cents 19 currency: 'usd', 20 description: item.description 21 }) 22 } 23 24 // Create and finalize invoice 25 const invoice = await stripe.invoices.create({ 26 customer: user.stripeCustomerId, 27 auto_advance: true, // Auto-finalize 28 collection_method: 'send_invoice', 29 days_until_due: 30 30 }) 31 32 // Send the invoice 33 await stripe.invoices.sendInvoice(invoice.id) 34 35 return invoice 36}

Best Practices#

  1. Store invoices locally - Keep a record for quick access
  2. Send receipt emails - Always confirm successful payments
  3. Handle failures gracefully - Notify users and provide update links
  4. Show invoice history - Let users access past invoices easily
  5. Support PDF downloads - Users need invoices for accounting