How to Build a Production-Ready Invoice Builder in .NET Using IronPDF
A simple, beginner-friendly guide to building a real invoice PDF generator in .NET with IronPDF, from HTML template to a clean download in your API.
Think about the bill you get at a sweet shop during Diwali. The shopkeeper does not draw a fresh bill format every time. There is one printed pad with boxes for the item names, the quantity, the rate, and the total. He just fills in the blanks for your order and tears off the page. Same shape, different numbers.
A invoice builder in .NET works the same way. You design the invoice once, as a template. Then for each customer you pour in fresh data, the items they bought, the prices, the GST, the total, and print a clean PDF. In this guide we will build exactly that using a library called IronPDF.
We will keep the words plain and the steps small. By the end you will have a real, working invoice generator that you could put into an ASP.NET Core API and ship.
Why a PDF, and why IronPDF?
A PDF looks the same everywhere. On your phone, on the customer's laptop, on the accountant's printer. An invoice has to look exactly right and never shift around, because money and tax are involved. That is the job a PDF does very well.
Now, the hard part is turning your data into a nice-looking PDF. You could draw every line and box by hand in code, but that is slow and painful. A much friendlier way is to write the invoice as an HTML page with CSS, the same way you would build a small web page, and then let a tool print that page to a PDF.
That is what IronPDF does. It carries its own copy of the Chrome rendering engine inside it. You hand it some HTML, and it prints a pixel-perfect PDF, the same way Chrome's "Print to PDF" works, but fully controlled from your C# code.
One honest note before we start. IronPDF is a commercial, paid library. It is not free for production. You get a 30-day free trial and a free development key to test everything, but for a real deployed app you need a license. We will talk about this clearly near the end so there are no surprises.
What we are building
We will build a small invoice service with three clear jobs:
- Hold the invoice data in plain C# objects.
- Turn that data into an HTML page that looks like a proper invoice.
- Use IronPDF to print that HTML into a PDF and return it.
Here is the shape of the whole thing.
Invoice builder pipeline
Steps
Data
Build Invoice object
Template
Fill HTML with values
Render
IronPDF makes PDF
Deliver
Save or download
Step 1: Install IronPDF
Add the NuGet package to your project. Use the .NET CLI:
dotnet add package IronPdfIronPDF works on .NET 10, 9, 8, and older versions too, on Windows, Linux, and macOS. For this guide we assume a normal ASP.NET Core Web API on .NET 10, which is the current LTS release.
Next, set your license key once when the app starts. During development you can use a trial key. In Program.cs:
using IronPdf;
// Set this once, at startup, before you render anything.
// Keep the real key in configuration or a secret, not in code.
License.LicenseKey = builder.Configuration["IronPdf:LicenseKey"];Notice we read the key from configuration. Never paste a real license key straight into your source code. Put it in appsettings.json, environment variables, or a secret store, so it does not leak into your Git history.
Step 2: Model the invoice data
Before any HTML, we need clean data. Let us describe an invoice with small C# records. Records are great here because an invoice, once made, should not change.
public record InvoiceLine(string Description, int Quantity, decimal UnitPrice)
{
public decimal LineTotal => Quantity * UnitPrice;
}
public record Invoice(
string Number,
string CustomerName,
DateOnly IssuedOn,
IReadOnlyList<InvoiceLine> Lines)
{
public decimal SubTotal => Lines.Sum(l => l.LineTotal);
public decimal Tax => Math.Round(SubTotal * 0.18m, 2); // 18% GST example
public decimal GrandTotal => SubTotal + Tax;
}A few things to notice. The LineTotal, SubTotal, Tax, and GrandTotal are computed properties. We never store the totals by hand. They are always worked out from the parts. This avoids a classic bug where the stored total and the line items disagree.
The 18% here is just an example GST rate. In a real app you would pass the rate in, because different items have different tax slabs.
Step 3: Turn data into HTML
Now we build the HTML. Think of it as filling the blanks on that sweet-shop bill pad. We will keep the layout in simple CSS so it prints cleanly.
public static class InvoiceHtmlBuilder
{
public static string Build(Invoice invoice)
{
var rows = string.Join("", invoice.Lines.Select(line => $"""
<tr>
<td>{line.Description}</td>
<td class="num">{line.Quantity}</td>
<td class="num">{line.UnitPrice:C}</td>
<td class="num">{line.LineTotal:C}</td>
</tr>
"""));
return $$"""
<html>
<head>
<style>
body { font-family: Arial, sans-serif; color: #222; }
h1 { color: #1a4f8b; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
.num { text-align: right; }
.total { font-weight: bold; font-size: 1.1em; }
</style>
</head>
<body>
<h1>Invoice {{invoice.Number}}</h1>
<p>Bill to: <strong>{{invoice.CustomerName}}</strong></p>
<p>Date: {{invoice.IssuedOn:dd MMM yyyy}}</p>
<table>
<tr><th>Item</th><th>Qty</th><th>Rate</th><th>Amount</th></tr>
{{rows}}
<tr class="total"><td colspan="3" class="num">Subtotal</td>
<td class="num">{{invoice.SubTotal:C}}</td></tr>
<tr><td colspan="3" class="num">GST (18%)</td>
<td class="num">{{invoice.Tax:C}}</td></tr>
<tr class="total"><td colspan="3" class="num">Grand Total</td>
<td class="num">{{invoice.GrandTotal:C}}</td></tr>
</table>
</body>
</html>
""";
}
}This is plain HTML and CSS. If you have ever made a small web page, this will feel familiar. The format shows the number as currency. The table grows or shrinks based on how many lines the invoice has.
For very large or fancy templates, many teams use Razor views instead of building strings by hand. That is a fine choice too. The idea is the same: data in, HTML out.
Step 4: Render the PDF with IronPDF
Here is the heart of the whole thing. We take the HTML and ask IronPDF to print it.
using IronPdf;
public class InvoicePdfService
{
private readonly ChromePdfRenderer _renderer;
public InvoicePdfService()
{
_renderer = new ChromePdfRenderer();
// Give the page a little breathing room around the edges.
_renderer.RenderingOptions.MarginTop = 20;
_renderer.RenderingOptions.MarginBottom = 20;
// Add a simple page number footer.
_renderer.RenderingOptions.TextFooter = new TextHeaderFooter
{
CenterText = "Page {page} of {total-pages}"
};
}
public byte[] Generate(Invoice invoice)
{
string html = InvoiceHtmlBuilder.Build(invoice);
PdfDocument pdf = _renderer.RenderHtmlAsPdf(html);
return pdf.BinaryData; // ready to save, stream, or download
}
}The key class is ChromePdfRenderer. The key method is RenderHtmlAsPdf. You pass HTML, you get back a PdfDocument. From there you can save it, stream it, merge it, or grab the raw bytes with BinaryData.
Notice we created the renderer once in the constructor and reused it. Starting up the Chrome engine takes a moment, so you do not want to do it on every single call. Build it once, use it many times.
Step 5: Wire it into an API endpoint
Now let us expose this so a browser or app can ask for an invoice and download it. Using a minimal API endpoint:
app.MapGet("/invoices/{number}/pdf", (string number, InvoicePdfService pdfService) =>
{
// In a real app you would load this from your database.
var invoice = new Invoice(
Number: number,
CustomerName: "Asha Traders",
IssuedOn: DateOnly.FromDateTime(DateTime.Today),
Lines: new[]
{
new InvoiceLine("Cotton T-Shirt", 3, 499m),
new InvoiceLine("Denim Jeans", 2, 1299m)
});
byte[] pdf = pdfService.Generate(invoice);
return Results.File(pdf, "application/pdf", $"invoice-{number}.pdf");
});Do not forget to register the service so it can be injected:
builder.Services.AddSingleton<InvoicePdfService>();We register it as a singleton because it holds a reusable renderer. One instance for the whole app is exactly what we want.
When someone visits /invoices/INV-1001/pdf, the API builds the data, renders the HTML, prints the PDF, and sends it back as a download. That is the full loop.
A picture of the request
Here is how a single request flows through the system, from click to download.
One PDF request
Steps
Browser
GET /pdf
Endpoint
Load invoice
Service
Build HTML
IronPDF
Render PDF
Download
Return file
Making it production-ready
A demo and a real service are not the same thing. Here are the things that matter once real customers are involved.
Reuse the renderer
We said this already, but it is worth repeating because it is the single biggest performance win. Do not create a new ChromePdfRenderer per request. Create it once and share it. Starting Chrome over and over will make your server slow and hungry for memory.
Handle bulk runs as background jobs
Generating one invoice inside a web request is fine. Generating 5000 invoices at the end of the month inside a single request is not. The request will time out and the server will struggle.
For bulk work, push each invoice job onto a queue and let a background worker chew through them. The web request returns instantly, and the heavy work happens quietly in the background.
Watch your memory
Each PDF render uses memory, and the Chrome engine is not tiny. If you generate a flood of PDFs at once, you can run out of memory. Limit how many run at the same time, for example with a SemaphoreSlim, so you never have a hundred renders fighting for RAM at the same moment.
Run it in containers carefully
On Linux and in Docker, the Chrome engine needs a few system libraries to be present. IronPDF can download what it needs, but the official Docker and Linux guides list the exact packages. Test your container early, not on launch day.
Common methods at a glance
IronPDF gives you a few rendering entry points. Pick the one that fits your source.
| Method | What you give it | Best for |
|---|---|---|
RenderHtmlAsPdf | An HTML string | Templates built in code, like our invoice |
RenderHtmlFileAsPdf | A path to an HTML file | Designs saved as static files |
RenderUrlAsPdf | A web URL | Printing an existing live page |
And here are the handful of options you will reach for most often.
| Option | What it controls |
|---|---|
MarginTop / MarginBottom | White space around the page |
TextFooter / TextHeader | Page numbers, dates, company name |
PaperSize | A4, Letter, and other sizes |
PrintHtmlBackgrounds | Whether CSS colors and images print |
Free trial and licensing, told honestly
IronPDF is a commercial product. Here is the plain truth so you can plan.
- You get a 30-day free trial and a free development key. You can test the full library, no features locked.
- For production you need a paid license.
- A perpetual license starts at around 749 US dollars at the low end, with higher tiers for more developers and projects.
- There is also a monthly subscription option if you prefer to pay over time.
Prices change, so always confirm on the official licensing page before you buy. If your project cannot pay for a library, look at free options like PuppeteerSharp or Playwright, which also print HTML to PDF using a headless browser. IronPDF's selling point is convenience, support, and a smooth single-package experience.
A common mistake to avoid
A very common slip is mixing up where the totals come from. Some developers store GrandTotal as a saved field and also store the line items. Then someone edits a line item, the total does not update, and the printed invoice shows numbers that do not add up. The customer notices, and it looks unprofessional.
The fix is the one we used above. Compute totals from the line items every time. Never store a number you can calculate. The invoice always tells the truth because the math happens at the last moment.
Quick recap
- An invoice is like a fill-in-the-blanks bill. Design once, pour in data, print.
- IronPDF prints HTML to PDF using a built-in Chrome engine, so you design with familiar HTML and CSS.
- Model your data with records and compute the totals, never store them by hand.
- Build the HTML, then call
ChromePdfRenderer.RenderHtmlAsPdfto get aPdfDocument. - Create the renderer once and reuse it. This is the biggest speed win.
- For bulk invoices, move the work to a background job so the web request stays fast.
- Watch memory, limit how many run at once, and test your Docker setup early.
- IronPDF is paid, with a 30-day trial. Confirm pricing on the official site before you ship.
References and further reading
- IronPDF HTML to PDF Tutorial (Official Docs)
- How to Generate PDF Dynamically in .NET Using IronPDF
- Generate PDF Invoices from HTML using IronPDF (TheCodeMan)
- IronPDF Licensing Terms (Official)
- IronPdf NuGet package
Related Posts
Flexible PDF Reporting in .NET Using Razor Views
A beginner-friendly guide to making PDF reports in .NET by writing Razor views as HTML and turning them into PDFs with a headless browser.
Building Resilient Cloud Applications With .NET
Learn to build resilient cloud apps in .NET with retries, timeouts, and circuit breakers using Polly and Microsoft.Extensions.Resilience.
How to Build a URL Shortener With .NET: A Beginner's Step-by-Step Guide
A friendly, step-by-step guide to building a URL shortener in .NET 10 using minimal APIs and EF Core. Learn short codes, redirects, and storage.
Build a Clean Architecture .NET App: A Hands-On PlaceOrder Tutorial
Build a Clean Architecture .NET 10 app from an empty solution to a working POST /orders minimal API. Four projects, one use case, EF Core, step by step.
How to Scale Long-Running API Requests in .NET: A Beginner's Guide
Learn how to handle slow, long-running API requests in .NET using the 202 Accepted pattern, background services, channels, and status polling.
Getting Started With Dapr for Building Cloud-Native Microservices in .NET
A beginner-friendly guide to Dapr for .NET developers: learn sidecars, state, pub/sub, and service invocation to build cloud-native microservices.