Typical scenario: A B2B SaaS product growing 20% monthly, but invoicing is still manual. Managers spend up to 15 minutes per invoice—copying data, pasting into Word, losing amounts, making mistakes in tax IDs. Clients complain about 2-3 day delays, accounting spends hours on reconciliation. Invoice automation reduces this process to 30 seconds—30x faster, saving up to $10,000 per year for a company with 500 monthly invoices. We implement the full cycle: from Stripe setup to custom PDF generation with S3 storage and 1C synchronization.
In a SaaS business with hundreds of clients and subscriptions in different currencies, manual invoicing becomes a bottleneck. Every error in details or taxes can cost thousands of dollars. We've seen projects where 15% of invoices had errors—after automation, that figure dropped to 0.5%. "Invoice automation saved us 40 hours per month and reduced late payments by 30%"—CFO of one client. Proper configuration of Stripe Invoicing and custom PDF generation not only speeds up the process but also builds client trust: invoices look professional and contain correct data.
Our team has implemented invoicing for 30+ SaaS projects, from startups to enterprise. We know all the pitfalls: from tax IDs to time zones. If you're facing growing invoice volumes, get a consultation with an engineer.
Why Invoice Automation Is Critical for SaaS
With each new client, the accounting workload increases. Errors in details, taxes, and currencies lead to payment delays and loss of trust. Automated invoicing eliminates human errors in invoice generation, speeds up issuance to seconds after subscription payment, supports multiple currencies (USD, EUR, RUB) and tax regimes (VAT, US sales tax), and generates custom-branded PDF invoices for each client. Integration with accounting automatically syncs data with 1C and other systems.
Problems We Solve
- Tax ambiguity — Stripe supports tax IDs (INN for RU, VAT for EU) but requires proper configuration. We set up automatic tax rate determination based on client country.
- Custom branding — Standard Stripe invoices rarely satisfy clients. We create custom PDF templates using @react-pdf/renderer, fully controlling design and field placement.
- Storage and delivery — PDF invoices must be accessible from the client portal and sent via email. We use S3 with pre-signed URLs and SendGrid for delivery.
- Accounting synchronization — via Stripe webhook or custom generation, we transfer data to 1C and other systems.
How to Ensure Tax Accuracy in Invoices
Tax rules differ by country and state. Stripe allows you to set tax rates globally, but for complex scenarios (e.g., EU Intra-community VAT), additional logic is required. We use the Stripe Tax API to automatically calculate taxes based on client address and product type. For custom PDFs, taxes are computed server-side using trusted libraries. Testing on real scenarios (Stripe sandbox) ensures every transaction passes without blocks.
How We Implemented Invoicing for a SaaS Startup
Client: US-based SaaS for freelancer management. Requirements: issue invoices in USD and EUR, with client logo, payment terms 15/30 days, and automatically save PDFs to S3. We chose a hybrid approach: subscriptions via Stripe Invoicing, PDF generation via React PDF with a custom component.
Example Stripe Customer Setup
// Customer configuration with invoice details
const customer = await stripe.customers.create({
email: '[email protected]',
name: 'Acme Corp',
address: {
line1: '1 Lenin St',
city: 'Moscow',
country: 'RU',
postal_code: '101000',
},
tax_ids: [{
type: 'ru_inn',
value: '7727563778',
}],
metadata: { tenantId },
});
// Custom branding via Stripe Dashboard:
// Settings → Branding → Logo, colors, footer text
// Update billing details by client
export async function updateBillingDetails(
tenantId: string,
data: BillingDetailsInput
): Promise<void> {
const subscription = await db.subscription.findUnique({
where: { tenantId }
});
await stripe.customers.update(subscription!.stripeCustomerId, {
name: data.companyName,
email: data.billingEmail,
address: {
line1: data.address,
city: data.city,
country: data.country,
postal_code: data.postalCode,
},
});
// Tax ID (INN for RU, VAT for EU)
if (data.taxId) {
// First delete old tax IDs
const existingCustomer = await stripe.customers.retrieve(
subscription!.stripeCustomerId,
{ expand: ['tax_ids'] }
) as Stripe.Customer;
for (const taxId of (existingCustomer.tax_ids as Stripe.ApiList<Stripe.TaxId>).data) {
await stripe.customers.deleteTaxId(subscription!.stripeCustomerId, taxId.id);
}
// Add new
await stripe.customers.createTaxId(subscription!.stripeCustomerId, {
type: data.taxIdType as Stripe.TaxIdCreateParams.Type,
value: data.taxId,
});
}
}
PDF generation via React PDF with S3 caching: the first request generates and stores, subsequent requests serve the cached file. This reduces load and speeds up delivery.
// npm install @react-pdf/renderer
import { pdf } from '@react-pdf/renderer';
import { InvoicePDF } from '@/components/pdf/InvoicePDF';
export async function generateInvoicePDF(invoiceId: string): Promise<Buffer> {
const invoice = await db.invoice.findUnique({
where: { id: invoiceId },
include: {
tenant: { include: { branding: true } },
lineItems: true,
}
});
const pdfStream = await pdf(
<InvoicePDF invoice={invoice!} />
).toBuffer();
return pdfStream;
}
// Save to S3 and return URL
export async function getInvoicePdfUrl(invoiceId: string): Promise<string> {
const key = `invoices/${invoiceId}.pdf`;
// Check if already exists
try {
await s3.headObject({ Bucket: process.env.AWS_BUCKET!, Key: key }).promise();
return `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`;
} catch {
// Not found — generate
}
const pdfBuffer = await generateInvoicePDF(invoiceId);
await s3.putObject({
Bucket: process.env.AWS_BUCKET!,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
ContentDisposition: `attachment; filename="invoice-${invoiceId}.pdf"`,
}).promise();
return `https://${process.env.AWS_BUCKET}.s3.amazonaws.com/${key}`;
}
The invoice component is detailed: a table with description, quantity, price, total; "From" and "To" blocks; payment status. All data comes from the database, ensuring accuracy.
// components/pdf/InvoicePDF.tsx
import {
Document, Page, Text, View, Image, StyleSheet
} from '@react-pdf/renderer';
const styles = StyleSheet.create({
page: { padding: 40, fontSize: 11, fontFamily: 'Helvetica' },
header: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 40 },
title: { fontSize: 24, fontWeight: 'bold' },
table: { marginTop: 20 },
tableRow: { flexDirection: 'row', borderBottom: '1px solid #eee', padding: '8px 0' },
tableHeader: { backgroundColor: '#f5f5f5', fontWeight: 'bold' },
});
export function InvoicePDF({ invoice }: { invoice: Invoice }) {
return (
<Document>
<Page size="A4" style={styles.page}>
<View style={styles.header}>
<View>
{invoice.tenant.branding?.logoUrl && (
<Image src={invoice.tenant.branding.logoUrl} style={{ height: 40 }} />
)}
<Text style={styles.title}>INVOICE</Text>
<Text>No. {invoice.number}</Text>
<Text>Date: {invoice.createdAt.toLocaleDateString('en-US')}</Text>
</View>
<View style={{ alignItems: 'flex-end' }}>
<Text style={{ fontSize: 18, color: '#6366f1' }}>
{formatCurrency(invoice.total, invoice.currency)}
</Text>
<Text style={{ color: invoice.status === 'paid' ? '#22c55e' : '#f59e0b' }}>
{invoice.status === 'paid' ? 'Paid' : 'Pending'}
</Text>
</View>
</View>
{/* Company details */}
<View style={{ flexDirection: 'row', gap: 40, marginBottom: 30 }}>
<View>
<Text style={{ fontWeight: 'bold', marginBottom: 4 }}>From:</Text>
<Text>{process.env.COMPANY_NAME}</Text>
<Text>Tax ID: {process.env.COMPANY_INN}</Text>
</View>
<View>
<Text style={{ fontWeight: 'bold', marginBottom: 4 }}>To:</Text>
<Text>{invoice.customerName}</Text>
{invoice.taxId && <Text>Tax ID: {invoice.taxId}</Text>}
</View>
</View>
{/* Invoice lines */}
<View style={styles.table}>
<View style={[styles.tableRow, styles.tableHeader]}>
<Text style={{ flex: 3 }}>Description</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>Quantity</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>Unit Price</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>Amount</Text>
</View>
{invoice.lineItems.map((item) => (
<View key={item.id} style={styles.tableRow}>
<Text style={{ flex: 3 }}>{item.description}</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>{item.quantity}</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>
{formatCurrency(item.unitAmount, invoice.currency)}
</Text>
<Text style={{ flex: 1, textAlign: 'right' }}>
{formatCurrency(item.amount, invoice.currency)}
</Text>
</View>
))}
</View>
{/* Total */}
<View style={{ alignItems: 'flex-end', marginTop: 20 }}>
<Text style={{ fontSize: 14, fontWeight: 'bold' }}>
Total: {formatCurrency(invoice.total, invoice.currency)}
</Text>
</View>
</Page>
</Document>
);
}
How Webhook Synchronization Works
The invoice.finalized webhook sends data about a new invoice. We upsert the record in our database and store the PDF URL from Stripe. For custom invoices, we generate the PDF and upload to S3. This gives the client real-time visibility of invoices.
case 'invoice.finalized': {
const stripeInvoice = event.data.object as Stripe.Invoice;
await db.invoice.upsert({
where: { stripeInvoiceId: stripeInvoice.id },
create: {
stripeInvoiceId: stripeInvoice.id,
tenantId: stripeInvoice.metadata.tenantId,
number: stripeInvoice.number!,
total: stripeInvoice.amount_due,
currency: stripeInvoice.currency,
status: 'open',
pdfUrl: stripeInvoice.invoice_pdf,
periodStart: new Date(stripeInvoice.period_start * 1000),
periodEnd: new Date(stripeInvoice.period_end * 1000),
},
update: { status: 'open' }
});
break;
}
What's Included
| Stage | Details |
|---|---|
| Analysis | Audit current process, agree on invoice format, tax requirements, currencies |
| Design | Architecture: choose between Stripe Invoicing and custom generation, webhook schema, data model |
| Implementation | Stripe setup, PDF template development, webhook handlers, S3 & email integration |
| Testing | Verify all scenarios: create, update, cancel, refund, different currencies |
| Documentation | API spec, accounting instructions, branding guide |
| Deploy | Production deployment, error monitoring, alert setup |
| Support | 1-month warranty (free bug fixes), optional team training |
Common Issues and Solutions
| Issue | Consequence | Solution |
|---|---|---|
| Incorrect tax ID | Tax not calculated | Validate format via Stripe validation |
| No PDF caching | High load | Store in S3 with pre-signed URLs |
| Ignoring idempotency | Duplicate invoices | Use idempotency_key |
| Not accounting for time zones | Date mismatches | Set client time zone |
Estimated Timelines
- Basic Stripe Invoicing configuration — 3 to 5 days (without custom PDFs).
- Custom PDF generation with S3 and webhooks — 7 to 10 days.
- Full cycle with 1C integration — 10 to 14 days.
Cost is calculated individually after an audit—contact us for a consultation.
Our experience: 10+ years in production, 40+ projects. We guarantee correct setup and support. Contact us to discuss your project and get a free consultation.







