Tutorials

Batch Invoice Generation at Scale: Complete API Guide

Build automated invoice generation for your SaaS. From template design to 10,000 invoices per hour with working code examples.

Typcraft TeamTypcraft Team
5 min read
Invoice generation API workflow diagram
#invoices#api#automation#saas

Every SaaS eventually needs to generate invoices. At first, you manually create them in Google Docs. Then you write a script. Then that script becomes a bottleneck.

This guide walks through building invoice automation that scales from 100 to 100,000 documents per month. We'll cover the architecture, the implementation, and the edge cases you'll hit in production.

The Invoice Automation Problem#

Manual invoice creation doesn't scale. The math is simple:

  • 5 minutes per invoice
  • 1,000 invoices per month
  • 83 hours of manual work

That's a full-time employee doing nothing but creating invoices.

But building invoice automation isn't just about speed. It's about:

  • Accuracy: No typos, no calculation errors
  • Consistency: Every invoice follows the same format
  • Auditability: Know exactly when each invoice was generated
  • Integration: Trigger generation from your billing system

Anatomy of an Invoice System#

Before writing code, understand the components:

StageComponentOutput
1. DataBilling SystemInvoice Data (JSON)
2. TemplateTemplate EngineTemplate + Data Binding
3. GenerationPDF EngineGenerated PDF
4. DeliveryDistributionEmail / Storage

Data Sources#

Your invoice data comes from somewhere:

  • Stripe: Subscription and usage-based billing
  • Your database: Custom billing logic
  • CRM: Customer details
  • Tax service: Tax rates and calculations

The key is normalizing this data into a consistent JSON structure.

Template#

The template defines the visual layout:

  • Company branding (logo, colors, fonts)
  • Invoice structure (header, line items, totals)
  • Data bindings that pull values from your JSON

Generation Engine#

The engine combines template + data to produce PDFs. This is where Typcraft fits in.

Delivery#

Generated invoices need to go somewhere:

  • Email to customer
  • Upload to S3/cloud storage
  • Attach to Stripe invoice
  • Download on demand

Building with Typcraft#

Let's build a complete invoice system.

Step 1: Design the Template#

In the Typcraft editor, create an invoice template with these elements:

Header Section

  • Your company logo and address
  • Invoice number and date
  • Due date
  • Customer billing address

Line Items Table

  • Description, quantity, unit price, total
  • Dynamic rows that expand based on data
  • Subtotal calculation

Footer Section

  • Tax breakdown
  • Total amount due
  • Payment instructions

The editor uses a visual approach to data binding. Instead of writing template syntax, you select elements and bind them to data paths:

ElementData Path
Invoice numberinvoice.number
Customer namecustomer.name
Line item description$item.description
Line item total$item.total

For tables, Typcraft automatically iterates over arrays. Bind a table to items, and each row uses $item to reference the current element, plus $index for the row number.

Step 2: Define Your Data Structure#

Create a TypeScript interface for invoice data:

TypeScript
interface InvoiceData {
  invoice: {
    number: string;
    date: string;
    dueDate: string;
    currency: string;
  };
  customer: {
    name: string;
    email: string;
    address: {
      line1: string;
      line2?: string;
      city: string;
      state: string;
      zip: string;
      country: string;
    };
  };
  items: Array<{
    description: string;
    quantity: string;
    unitPrice: string;
    total: string;
  }>;
  subtotal: string;
  tax: {
    rate: string;
    amount: string;
  };
  total: string;
}

Important: All values should be pre-formatted as strings. The template displays exactly what you send. Format numbers and dates before sending.

Step 3: Generate a Single Invoice#

Typcraft uses a REST API. Here's how to generate a single invoice:

TypeScript
async function generateInvoice(data: InvoiceData): Promise<Blob> {
  const response = await fetch('https://typcraft.com/api/v1/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      templateId: 'your-invoice-template-id',
      format: 'pdf',
      data: data,
      sync: true,
    }),
  });
 
  if (!response.ok) {
    throw new Error(`Generation failed: ${response.statusText}`);
  }
 
  return response.blob();
}
 
// Usage
const invoiceData: InvoiceData = {
  invoice: {
    number: 'INV-2026-0001',
    date: 'January 24, 2026',
    dueDate: 'February 23, 2026',
    currency: 'USD',
  },
  customer: {
    name: 'Acme Corporation',
    email: '[email protected]',
    address: {
      line1: '123 Business Ave',
      city: 'San Francisco',
      state: 'CA',
      zip: '94102',
      country: 'US',
    },
  },
  items: [
    {
      description: 'Pro Plan - Monthly',
      quantity: '1',
      unitPrice: '$29.00',
      total: '$29.00',
    },
    {
      description: 'Additional Users (5)',
      quantity: '5',
      unitPrice: '$5.00',
      total: '$25.00',
    },
  ],
  subtotal: '$54.00',
  tax: {
    rate: '8.75%',
    amount: '$4.73',
  },
  total: '$58.73',
};
 
const pdfBlob = await generateInvoice(invoiceData);

Step 4: Batch Generation#

For monthly billing runs, you'll generate hundreds or thousands of invoices. The approach: submit jobs in parallel, then collect results.

TypeScript
interface GenerationJob {
  jobId: string;
  customerId: string;
}
 
async function submitGenerationJob(
  templateId: string,
  data: InvoiceData,
  customerId: string
): Promise<GenerationJob> {
  const response = await fetch('https://typcraft.com/api/v1/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      templateId,
      format: 'pdf',
      data,
    }),
  });
 
  const { jobId } = await response.json();
  return { jobId, customerId };
}
 
async function pollJobStatus(jobId: string): Promise<{
  status: 'pending' | 'processing' | 'completed' | 'failed';
  downloadUrl?: string;
  error?: string;
}> {
  const response = await fetch(
    `https://typcraft.com/api/v1/jobs/${jobId}`,
    {
      headers: {
        'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
      },
    }
  );
  return response.json();
}
 
async function generateMonthlyInvoices() {
  // Fetch all customers with unpaid balances
  const customers = await db.customers.findMany({
    where: {
      balance: { gt: 0 },
      status: 'active',
    },
    include: {
      subscriptions: true,
      usage: true,
    },
  });
 
  console.log(`Generating ${customers.length} invoices...`);
 
  // Submit all jobs in parallel
  const jobs = await Promise.all(
    customers.map((customer) =>
      submitGenerationJob(
        'invoice-template-id',
        transformToInvoiceData(customer),
        customer.id
      )
    )
  );
 
  console.log(`Submitted ${jobs.length} jobs`);
 
  // Poll for completion
  const results = await Promise.all(
    jobs.map(async (job) => {
      let status = await pollJobStatus(job.jobId);
 
      // Poll until complete (with timeout)
      const maxAttempts = 30;
      let attempts = 0;
 
      while (
        status.status !== 'completed' &&
        status.status !== 'failed' &&
        attempts < maxAttempts
      ) {
        await sleep(1000);
        status = await pollJobStatus(job.jobId);
        attempts++;
      }
 
      return {
        customerId: job.customerId,
        jobId: job.jobId,
        ...status,
      };
    })
  );
 
  // Process results
  for (const result of results) {
    if (result.status === 'completed' && result.downloadUrl) {
      await db.invoices.update({
        where: { customerId: result.customerId },
        data: {
          pdfUrl: result.downloadUrl,
          generatedAt: new Date(),
        },
      });
 
      await sendInvoiceEmail(result.customerId, result.downloadUrl);
    } else if (result.status === 'failed') {
      console.error(`Invoice failed for ${result.customerId}: ${result.error}`);
      await alertOps(`Invoice generation failed for ${result.customerId}`);
    }
  }
 
  console.log(`Completed: ${results.filter(r => r.status === 'completed').length}`);
  console.log(`Failed: ${results.filter(r => r.status === 'failed').length}`);
}
 
function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

For very large batches, consider using a job queue like BullMQ to manage the workflow with better retry handling and rate limiting.

Performance at Scale#

Real numbers from production systems:

VolumeGeneration TimeAPI Cost (Typcraft)
100 invoices~10 seconds$1.50
1,000 invoices~90 seconds$15.00
10,000 invoices~12 minutes$150.00

Compare this to building in-house:

  • Developer time: 40-80 hours
  • Server costs: $50-200/month for PDF generation infrastructure
  • Maintenance: Ongoing debugging, updates, scaling

For most SaaS companies, the API approach pays for itself quickly.

Edge Cases#

Production invoice systems need to handle:

Multi-Page Invoices#

When a customer has 100+ line items, the invoice spans multiple pages. Your template needs:

  • Repeating headers on each page
  • Page numbers ("Page 1 of 3")
  • Totals only on the last page

Typcraft handles pagination automatically. Design your template with page headers and footers, and the engine breaks pages correctly.

Multi-Currency#

International customers need invoices in their local currency. Format amounts before sending:

TypeScript
function formatCurrency(amount: number, currency: string): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}
 
const invoiceData = {
  // ...
  total: formatCurrency(58.73, customer.currency), // "58.73 EUR"
};

Tax Jurisdictions#

Tax rules vary by location:

  • US: State and local sales tax
  • EU: VAT with reverse charge for B2B
  • Canada: GST/HST/PST combinations

Integrate with a tax service (Avalara, TaxJar) or build jurisdiction logic:

TypeScript
function calculateTax(customer: Customer, amount: number) {
  if (customer.country === 'US') {
    return calculateUSSalesTax(customer.state, amount);
  } else if (isEUCountry(customer.country)) {
    return calculateVAT(customer, amount);
  }
  return { rate: 0, amount: 0 };
}

PDF/A for Archival#

Some industries require PDF/A format for long-term archival:

TypeScript
const response = await fetch('https://typcraft.com/api/v1/generate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    templateId: 'invoice-template-id',
    format: 'pdf',
    data: invoiceData,
    options: {
      pdfA: true,
    },
  }),
});

Integration Patterns#

Stripe Webhook#

Generate invoices automatically when Stripe finalizes an invoice:

TypeScript
// POST /api/webhooks/stripe
export async function POST(req: Request) {
  const event = await stripe.webhooks.constructEvent(
    await req.text(),
    req.headers.get('stripe-signature')!,
    process.env.STRIPE_WEBHOOK_SECRET!
  );
 
  if (event.type === 'invoice.finalized') {
    const stripeInvoice = event.data.object;
    const invoiceData = transformStripeInvoice(stripeInvoice);
 
    const response = await fetch('https://typcraft.com/api/v1/generate', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        templateId: 'invoice-template-id',
        format: 'pdf',
        data: invoiceData,
        sync: true,
      }),
    });
 
    const pdfBlob = await response.blob();
    const pdfUrl = await uploadToS3(pdfBlob, `invoices/${stripeInvoice.id}.pdf`);
 
    await stripe.invoices.update(stripeInvoice.id, {
      metadata: { pdfUrl },
    });
  }
 
  return Response.json({ received: true });
}

Monthly Cron Job#

For custom billing, run invoice generation on a schedule:

TypeScript
// Triggered by cron: 0 6 1 * * (6 AM on the 1st of each month)
export async function monthlyBillingJob() {
  await generateMonthlyInvoices();
 
  await db.billingRuns.create({
    data: {
      completedAt: new Date(),
      status: 'completed',
    },
  });
}

On-Demand Download#

Let customers download invoices from your dashboard:

TypeScript
// GET /api/invoices/:id/pdf
export async function GET(req: Request, { params }: { params: { id: string } }) {
  const invoice = await db.invoices.findUnique({
    where: { id: params.id },
    include: { customer: true },
  });
 
  if (!invoice) {
    return Response.json({ error: 'Not found' }, { status: 404 });
  }
 
  // Check authorization
  const session = await getSession(req);
  if (invoice.customerId !== session.customerId) {
    return Response.json({ error: 'Forbidden' }, { status: 403 });
  }
 
  // Return cached PDF if available
  if (invoice.pdfUrl) {
    return Response.redirect(invoice.pdfUrl);
  }
 
  // Generate on demand
  const response = await fetch('https://typcraft.com/api/v1/generate', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.TYPCRAFT_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      templateId: 'invoice-template-id',
      format: 'pdf',
      data: buildInvoiceData(invoice),
      sync: true,
    }),
  });
 
  const pdfBlob = await response.blob();
  const pdfUrl = await uploadToS3(pdfBlob, `invoices/${invoice.id}.pdf`);
 
  // Cache for future requests
  await db.invoices.update({
    where: { id: params.id },
    data: { pdfUrl },
  });
 
  return Response.redirect(pdfUrl);
}

Checklist Before Going Live#

  • Template matches your brand guidelines
  • All legal requirements included (tax IDs, terms)
  • Data validation catches malformed input
  • Error handling notifies your team of failures
  • Rate limiting prevents API abuse
  • PDF/A compliance if required by your industry
  • Email delivery tested with real addresses
  • Invoice numbers are unique and sequential
  • Currency formatting correct for all locales

The Bottom Line#

Invoice automation isn't just about saving time. It's about building a billing system that scales with your business.

Start simple: one template, single generation, manual trigger. Then add batch processing and integrations as you grow.

The code in this guide is production-ready. Adapt it to your billing system and start generating.


Ready to automate your invoices? Start with Typcraft's free tier and generate your first invoice in minutes.

Typcraft Team

Written by

Typcraft Team

Building the next generation of document automation.

@typcraftapp

Continue Reading