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 stripeGet 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#
- Store invoices locally - Keep a record for quick access
- Send receipt emails - Always confirm successful payments
- Handle failures gracefully - Notify users and provide update links
- Show invoice history - Let users access past invoices easily
- Support PDF downloads - Users need invoices for accounting
Related Patterns#
- Stripe - Stripe setup
- Webhooks - Webhook handling
- Subscriptions - Subscription invoices