How to Create a PDF Invoice from an HTML Template in C#

PDF.co can generate PDF invoices by combining a reusable HTML template with invoice data supplied by your application.

In this tutorial, you will:

  1. Create an HTML invoice template in PDF.co.
  2. Add Handlebars placeholders for dynamic invoice data.
  3. Create a C# console application.
  4. Send the template ID and invoice data to the PDF.co API.
  5. Download the generated PDF.

The example uses modern .NET, HttpClient, and the built-in System.Text.Json library.

Prerequisites

Before beginning, make sure you have:

  • A PDF.co account
  • A PDF.co API key
  • The .NET SDK installed
  • A C# development environment, such as Visual Studio, Visual Studio Code, or JetBrains Rider

Do not place your API key directly in application source code. Store it in an environment variable or a secure secrets-management service.

Step 1: Create an HTML Template

Sign in to your PDF.co account and open the HTML to PDF Templates section:

https://app.pdf.co/templates/html

Create a new template and enter a descriptive name, such as Invoice Template.

You can write your own HTML or adapt an existing template. PDF.co templates support Mustache and Handlebars syntax.

Use double braces to insert individual values:

<h1>Invoice {{invoice_id}}</h1>
<p>Invoice date: {{invoice_date}}</p>
<p>Customer: {{client_name}}</p>
<p>Total: ${{total}}</p>

Use the #if helper to conditionally display content:

{{#if paid}}
<div class="paid-status">PAID</div>
{{/if}}

Use the #each helper to repeat markup for an array of invoice items:

<table>
    <thead>
        <tr>
            <th>Description</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
        {{#each items}}
        <tr>
            <td>{{name}}</td>
            <td>${{price}}</td>
        </tr>
        {{/each}}
    </tbody>
</table>

A simple complete invoice template might look like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Invoice {{invoice_id}}</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            color: #222;
            margin: 0;
        }

        .invoice-header {
            display: flex;
            justify-content: space-between;
            margin-bottom: 32px;
        }

        .paid-status {
            color: #188038;
            border: 2px solid #188038;
            padding: 6px 12px;
            font-weight: bold;
        }

        .address-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 40px;
            margin-bottom: 30px;
        }

        table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 24px;
        }

        th,
        td {
            padding: 10px;
            border-bottom: 1px solid #ddd;
            text-align: left;
        }

        th:last-child,
        td:last-child {
            text-align: right;
        }

        .totals {
            margin-top: 24px;
            margin-left: auto;
            width: 280px;
        }

        .totals p {
            display: flex;
            justify-content: space-between;
        }

        .total {
            font-size: 18px;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="invoice-header">
        <div>
            <h1>Invoice {{invoice_id}}</h1>
            <p>Issued: {{invoice_date}}</p>
            <p>Due: {{invoice_date_due}}</p>
        </div>

        {{#if paid}}
        <div class="paid-status">PAID</div>
        {{/if}}
    </div>

    <div class="address-grid">
        <div>
            <h2>From</h2>
            <p>
                {{issuer_name}}<br>
                {{issuer_company}}<br>
                {{issuer_address}}<br>
                {{issuer_email}}
            </p>
        </div>

        <div>
            <h2>Bill to</h2>
            <p>
                {{client_name}}<br>
                {{client_company}}<br>
                {{client_address}}<br>
                {{client_email}}
            </p>
        </div>
    </div>

    <table>
        <thead>
            <tr>
                <th>Description</th>
                <th>Price</th>
            </tr>
        </thead>
        <tbody>
            {{#each items}}
            <tr>
                <td>{{name}}</td>
                <td>${{price}}</td>
            </tr>
            {{/each}}
        </tbody>
    </table>

    <div class="totals">
        <p><span>Subtotal:</span><span>${{subtotal}}</span></p>
        <p><span>Discount:</span><span>-${{discount}}</span></p>
        <p><span>Tax:</span><span>${{tax}}</span></p>
        <p class="total"><span>Total:</span><span>${{total}}</span></p>
    </div>

    <p>{{note}}</p>
</body>
</html>

Save the template when you are satisfied with its content.

Step 2: Copy the Template ID

After saving the template, locate its template ID in the HTML template list or editor.

The ID is specific to your PDF.co account. Do not assume that a built-in template ID from another account will be available in yours.

Copy the ID because it must be supplied as the templateId value in the API request.

Step 3: Create a C# Console Application

Create a new console project:

dotnet new console -n PdfInvoiceGenerator
cd PdfInvoiceGenerator

This tutorial uses libraries included with modern .NET, so no additional JSON package is required.

Step 4: Store the PDF.co API Key Securely

Save your API key in an environment variable named PDFCO_API_KEY.

On macOS or Linux:

export PDFCO_API_KEY="YOUR_PDFCO_API_KEY"

In Windows PowerShell:

$env:PDFCO_API_KEY="YOUR_PDFCO_API_KEY"

Replace the placeholder with the API key from your PDF.co dashboard.

For production applications, use a suitable secrets-management service rather than storing the key in a script, repository, or published tutorial.

Step 5: Add the C# Code

Replace the contents of Program.cs with the following code.

Update TemplateId so it matches the HTML template saved in your PDF.co account.

using System.Net.Http.Json;
using System.Text.Json;

internal class Program
{
    private const string Endpoint =
        "https://api.pdf.co/v1/pdf/convert/from/html";

    private const string OutputFile = "invoice.pdf";

    // Replace this with the template ID from your PDF.co account.
    private const int TemplateId = 1;

    private static async Task Main()
    {
        string apiKey =
            Environment.GetEnvironmentVariable("PDFCO_API_KEY")
            ?? throw new InvalidOperationException(
                "Set the PDFCO_API_KEY environment variable before running the application.");

        var invoiceData = new
        {
            paid = true,
            invoice_id = "INV-0021",
            invoice_date = "August 29, 2041",
            invoice_date_due = "September 29, 2041",

            issuer_name = "Sarah Connor",
            issuer_company = "T-800 Research Lab",
            issuer_address = "435 South La Fayette Park Place, Los Angeles, CA 90057",
            issuer_email = "billing@example.com",

            client_name = "Cyberdyne Systems",
            client_company = "Cyberdyne Systems",
            client_address = "18144 El Camino Real, Sunnyvale, California",
            client_email = "accounts@example.com",

            items = new[]
            {
                new
                {
                    name = "T-800 Prototype Research",
                    price = 1000.00m
                },
                new
                {
                    name = "Cloud Synchronization Setup",
                    price = 300.00m
                }
            },

            subtotal = 1300.00m,
            discount = 100.00m,
            tax = 87.00m,
            total = 1287.00m,
            note = "Thank you for your business."
        };

        var requestBody = new
        {
            templateId = TemplateId,
            templateData = JsonSerializer.Serialize(invoiceData),
            name = OutputFile,
            mediaType = "print",
            margins = "40px 20px 20px 20px",
            paperSize = "Letter",
            orientation = "Portrait",
            printBackground = true,
            async = false
        };

        using var apiClient = new HttpClient();
        apiClient.DefaultRequestHeaders.Add("x-api-key", apiKey);

        using HttpResponseMessage response =
            await apiClient.PostAsJsonAsync(Endpoint, requestBody);

        string responseBody = await response.Content.ReadAsStringAsync();

        if (!response.IsSuccessStatusCode)
        {
            throw new HttpRequestException(
                $"PDF.co returned HTTP {(int)response.StatusCode}: {responseBody}");
        }

        using JsonDocument responseJson = JsonDocument.Parse(responseBody);
        JsonElement result = responseJson.RootElement;

        if (result.TryGetProperty("error", out JsonElement error) &&
            error.GetBoolean())
        {
            string message =
                result.TryGetProperty("message", out JsonElement messageElement)
                    ? messageElement.GetString() ?? "Unknown PDF.co error."
                    : "Unknown PDF.co error.";

            throw new InvalidOperationException(message);
        }

        string resultUrl =
            result.GetProperty("url").GetString()
            ?? throw new InvalidOperationException(
                "The PDF.co response did not contain an output URL.");

        // Use a separate client so the API key is not sent to temporary storage.
        using var downloadClient = new HttpClient();
        byte[] pdfBytes = await downloadClient.GetByteArrayAsync(resultUrl);

        await File.WriteAllBytesAsync(OutputFile, pdfBytes);

        Console.WriteLine($"Invoice saved to: {Path.GetFullPath(OutputFile)}");
    }
}

Step 6: Understand the Request

The application sends a POST request to:

https://api.pdf.co/v1/pdf/convert/from/html

The request includes the following important properties:

  • templateId: The ID of the saved HTML template.
  • templateData: A JSON string containing values for the template placeholders.
  • name: The output PDF file name.
  • paperSize: The requested page size.
  • orientation: The page orientation.
  • margins: The top, right, bottom, and left margins.
  • mediaType: Determines which CSS media rules are applied.
  • printBackground: Includes background colors and images when set to true.
  • async: Uses synchronous processing when set to false.

The property names inside invoiceData must match the placeholders in the HTML template.

For example:

{{invoice_id}}

is populated by:

invoice_id = "INV-0021"

The items array is rendered by the Handlebars #each block in the template.

Step 7: Run the Application

Run the application:

dotnet run

The program will:

  1. Read the API key from the environment.
  2. Serialize the invoice data to JSON.
  3. Send the template ID and data to PDF.co.
  4. Receive a temporary URL for the generated PDF.
  5. Download the file as invoice.pdf.

Open invoice.pdf and confirm that the invoice contains the expected customer, issuer, item, and total information.

PDF.co output URLs are temporary. This example downloads the generated PDF immediately so it remains available after the temporary URL expires.

Step 8: Test Conditional Content

The HTML template uses this conditional block:

{{#if paid}}
<div class="paid-status">PAID</div>
{{/if}}

When the C# data contains:

paid = true

the PDF displays the PAID label.

Change the value to:

paid = false

Run the application again. The new PDF will omit the label.

Conditional sections can also be used for payment instructions, overdue notices, optional notes, discounts, and other content that should appear only under specific conditions.

Step 9: Customize the Invoice

You can expand the template and data model to include:

  • A company logo
  • Invoice currency
  • Customer tax information
  • Shipping and billing addresses
  • Product quantities
  • Unit prices
  • Payment terms
  • Purchase-order numbers
  • Page numbers
  • Custom headers and footers

If a header or footer occupies additional space, increase the corresponding page margin so that it does not overlap the invoice content.

For large or long-running conversions, set async to true and check the returned job ID with PDF.co’s background-job endpoint.

You have created a C# application that generates a PDF invoice from a reusable HTML template using PDF.co.

The application sends structured invoice data to the PDF.co HTML-to-PDF endpoint, fills the template’s Handlebars placeholders, downloads the generated PDF, and stores it locally.

Keeping the template in PDF.co separates invoice design from application logic. You can update the HTML and CSS without rebuilding the C# application, provided that the placeholder names remain compatible with the data sent by your code.

Related Tutorials

See Related Tutorials