IRONSOFTWAREHOME
USING IRONPDF

How to Build a Centralized PDF Generation Service with ASP.NET Core and IronPDF

Curtis Chau
Curtis Chau
Updated: August 1, 2026

Build a production-ready .NET PDF API using ASP.NET Core and IronPDF to centralize PDF generation logic, enabling consistent document creation across your applications with RESTful endpoints for HTML-to-PDF conversion, merging, watermarking, and dynamic template processing.

When working with modern applications, .NET developers often need to build a centralized PDF generation service. Whether you're generating invoices, reports, certificates, or contracts, having a dedicated .NET PDF API improves your PDF generation workflow. How does it help? It provides consistency, maintainability, and scalability across your desktop and web applications. Managing document content, PDF pages, and PDF form fields becomes straightforward.

In this tutorial, you'll learn how to build a production-ready PDF API using ASP.NET Core and IronPDF, a powerful .NET PDF library. We'll create RESTful endpoints that generate PDFs from HTML, merge documents, add watermarks, and handle various real-world PDF scenarios in your Web API.

Why Build a Dedicated PDF API?

Architecture diagram showing client apps (web, desktop, mobile) sending HTML content, URLs, or dynamic data to a PDF Controller/Service API layer, which outputs PDF documents.

Before diving into the code, let's understand why creating a dedicated PDF API makes sense:

  • Centralized Logic: All PDF generation logic lives in one place, simplifying maintenance and updates.
  • Microservice Architecture: Perfect for service-oriented architectures where different apps need PDF capabilities.
  • Performance Optimization: Easier to scale and optimize a dedicated service for large PDFs, multiple pages, and dynamic data using async operations and performance techniques.
  • Language Agnostic: Any client application can consume the API regardless of programming language.
  • Consistent Output: Ensures all PDFs across your organization maintain consistent layout, formatting, and content.

Ready to start building? Download IronPDF's free trial and follow along with this tutorial to programmatically create PDF files in your .NET Framework projects.

What Makes IronPDF the Complete .NET PDF Library?

IronPDF for .NET homepage showing C# code examples for converting HTML to PDF with features like HTML rendering, file saving, and NuGet installation command

IronPDF stands out as the premier PDF library for .NET developers, offering a comprehensive set of features that make PDF generation in Web API projects straightforward and reliable. Built on a Chrome rendering engine, it ensures pixel-perfect HTML-to-PDF conversions in just a few lines of code while maintaining all styling, JavaScript execution, and responsive layouts.

Key capabilities that make IronPDF ideal for .NET PDF API development:

How to Set Up Your PDF Document API Project?

Let's start by creating a new ASP.NET Core Web API project and installing the necessary packages.

What Are the Prerequisites?

  • .NET 6.0 SDK or later
  • Visual Studio 2022 or Visual Studio Code
  • Postman or similar API testing tool for testing your PDF REST API

How Do I Create the Project?

First, let's create the project where we'll build our PDF generation tool.

dotnet new webapi -n PdfApiService
cd PdfApiService
SHELL

How Do I Install IronPDF?

Next, add IronPDF to your project via NuGet:

dotnet add package IronPDF
SHELL

Or, using the NuGet Package Manager Console in Visual Studio:

PM > Install-Package IronPdf

For advanced installation options, including platform-specific packages, Docker setup, or Linux configurations, check the IronPDF installation documentation.

What Project Structure Should I Use?

Good C# development requires maintaining a clean and well-structured project folder. For example:

Visual Studio Solution Explorer showing the folder structure of a .NET PDF API Service project with Controllers, Models, and Services directories

How to Create Your First PDF Endpoint?

Let's build a simple endpoint that converts HTML to PDF format. First, create the service interface and implementation:

How Do I Create the PDF Service?

First, add the following to your IPdfService.cs file:

public interface IPdfService
{
    byte[] GeneratePdfFromHtml(string htmlContent);
    byte[] GeneratePdfFromUrl(string url);
}

In the PdfService.cs file, add this:

using IronPdf;
public class PdfService : IPdfService
{
    private readonly ChromePdfRenderer _renderer;
    public PdfService()
    {
        _renderer = new ChromePdfRenderer();
        // Configure rendering options for optimal PDF generation in .NET
        _renderer.RenderingOptions.MarginTop = 20;
        _renderer.RenderingOptions.MarginBottom = 20;
        _renderer.RenderingOptions.PrintHtmlBackgrounds = true;
    }
    public byte[] GeneratePdfFromHtml(string htmlContent)
    {
        // Generate PDF from HTML using the .NET PDF API
        var pdf = _renderer.RenderHtmlAsPdf(htmlContent);
        return pdf.BinaryData;
    }
    public byte[] GeneratePdfFromUrl(string url)
    {
        // Convert URL to PDF in the REST API
        var pdf = _renderer.RenderUrlAsPdf(url);
        return pdf.BinaryData;
    }
}

The PdfService handles converting HTML into PDF. Using IronPDF's ChromePdfRenderer, this class configures defaults like page margins and background rendering for professional results. For advanced rendering configurations, explore IronPDF's rendering options.

When the controller passes raw HTML, the service renders it into a high-quality PDF and returns the byte data for download. It also converts entire web pages directly into PDFs using URL to PDF conversion.

How Do I Create the Controller?

Now create the controller for your API. This provides an endpoint that generates PDF files from HTML and lets you download and save PDF documents to your system.

// Controllers/PdfController.cs
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class PdfController : ControllerBase
{
    private readonly IPdfService _pdfService;
    public PdfController(IPdfService pdfService)
    {
        _pdfService = pdfService;
    }
    [HttpPost("html-to-pdf")]
    public IActionResult ConvertHtmlToPdf([FromBody] HtmlRequest request)
    {
        try
        {
            var pdfBytes = _pdfService.GeneratePdfFromHtml(request.HtmlContent);
            // Return as downloadable file
            return File(pdfBytes, "application/pdf", "document.pdf");
        }
        catch (Exception ex)
        {
            return BadRequest($"Error generating PDF: {ex.Message}");
        }
    }
}

Then, in the HtmlRequest.cs file, add this:

// Models/HtmlRequest.cs
public class HtmlRequest
{
    public string HtmlContent { get; set; }
    public string FileName { get; set; } = "document.pdf";
}

This sets up an API endpoint that converts HTML into a downloadable PDF. When someone sends HTML to api/pdf/html-to-pdf, the PdfController delegates conversion to the service.

Once created, the controller returns the PDF as a downloadable file. The request uses the HtmlRequest model, containing the HTML and an optional filename. This makes it easy for clients to send HTML and receive a polished PDF.

How Do I Register Services?

Update your Program.cs to register the PDF service:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register PDF service
builder.Services.AddSingleton<IPdfService, PdfService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapControllers();
app.Run();

How to Handle Different Response Types?

Your API should support different ways of returning PDFs based on client needs:

[HttpPost("generate")]
public IActionResult GeneratePdf([FromBody] PdfRequest request)
{
    var pdfBytes = _pdfService.GeneratePdfFromHtml(request.HtmlContent);
    switch (request.ResponseType?.ToLower())
    {
        case "base64":
            return Ok(new
            {
                data = Convert.ToBase64String(pdfBytes),
                filename = request.FileName
            });
        case "inline":
            return File(pdfBytes, "application/pdf");
        default: // download
            return File(pdfBytes, "application/pdf", request.FileName);
    }
}

This adds a flexible PDF generation endpoint. Instead of forcing downloads, the GeneratePdf method lets clients choose how they receive results: as a download, inline in browser, or Base64 encoded for API use.

The PdfRequest model extends HtmlRequest with a ResponseType option. This gives users control over PDF delivery, making the API more versatile. For handling PDFs in memory without file system access, see IronPDF's memory stream documentation.

When we run our program, we'll see this output on Swagger:

Swagger UI documentation showing PdfApiService endpoints with three POST methods for PDF operations: html-to-pdf, generate, and url-to-pdf, along with request schemas.

How to Implement Common PDF Operations?

Let's expand our service to handle various PDF generation scenarios:

How Do I Convert URLs to PDF?

[HttpPost("url-to-pdf")]
public async Task<IActionResult> ConvertUrlToPdf([FromBody] UrlRequest request)
{
    try
    {
        var pdfBytes = await Task.Run(() =>
            _pdfService.GeneratePdfFromUrl(request.Url));
        return File(pdfBytes, "application/pdf",
            $"{request.FileName ?? "website"}.pdf");
    }
    catch (Exception ex)
    {
        return BadRequest($"Failed to convert URL: {ex.Message}");
    }
}
public class UrlRequest
{
    public string Url { get; set; }
    public string FileName { get; set; }
}

This endpoint converts URLs into downloadable PDFs. When a POST request hits /api/pdf/url-to-pdf, the controller uses _pdfService to convert the URL into PDF bytes in the background, then returns them for download. If conversion fails, it responds with a clear error message. For websites with authentication, check out IronPDF's login documentation.

Let's try using the URL "https://www.apple.com/nz" and test the POST request. Below is the output we obtained:

What Does the Output Look Like?

Apple New Zealand website homepage showing multiple product sections including iPhone 16, MacBook Air, Apple Watch, and AirPods with 'Learn more' and 'Buy' buttons - demonstrating URL to PDF conversion

How Do I Add Custom Watermarks?

public byte[] AddWatermarkFromFile(string filePath, string watermarkText)
{
    // Load PDF directly from file
    var pdf = PdfDocument.FromFile(filePath);
    pdf.ApplyWatermark(
        $"<h1 style='color:red;font-size:72px;'>{watermarkText}</h1>",
        75,
        IronPdf.Editing.VerticalAlignment.Middle,
        IronPdf.Editing.HorizontalAlignment.Center
    );
    return pdf.BinaryData;
}

This manually loads a local file for testing. You can adjust this so your PDF API generates a PDF, then applies custom watermarks easily. For advanced watermarking options, including image watermarks and custom positioning, see the watermarking guide.

What Does the Watermark Output Look Like?

A secure document with 'CONFIDENTIAL' watermark displayed diagonally across a blank page, with Document ID: BA811648DCE1FF2AAA55E7CE shown at the top

How to Add Dynamic Data with Templates?

For real-world applications, you'll often need to generate PDFs from templates with dynamic data:

[HttpPost("from-template")]
public IActionResult GenerateFromTemplate([FromBody] TemplateRequest request)
{
    // Simple template replacement
    var html = request.Template;
    foreach (var item in request.Data)
    {
        html = html.Replace($"{{{{{item.Key}}}}}", item.Value);
    }
    var pdfBytes = _pdfService.GeneratePdfFromHtml(html);
    return File(pdfBytes, "application/pdf", request.FileName);
}
public class TemplateRequest
{
    public string Template { get; set; }
    public Dictionary<string, string> Data { get; set; }
    public string FileName { get; set; } = "document.pdf";
}

For advanced template scenarios with Razor, Handlebars, or other engines, check out IronPDF's HTML to PDF documentation. You can also explore CSHTML to PDF conversion for MVC applications and Razor to PDF for Blazor applications. For headless Razor rendering, see the CSHTML headless guide.

How to Optimize Performance?

When building a production PDF API, performance is crucial. Here are key optimization strategies:

Why Use Async Operations?

Use asynchronous coding when your projects involve I/O operations. This helps especially when your PDF content comes from external resources like:

  • Downloading HTML pages (RenderUrlAsPdf)
  • Fetching images, CSS, or fonts over HTTP
  • Reading/writing files to disk or cloud storage

These operations can block threads, but async prevents API threads from waiting idle. For comprehensive async PDF generation patterns, see the async PDF generation guide.

Example:

public async Task<byte[]> GeneratePdfFromHtmlAsync(string htmlContent)
{
    return await Task.Run(() =>
    {
        var pdf = _renderer.RenderHtmlAsPdf(htmlContent);
        return pdf.BinaryData;
    });
}

For parallel PDF generation scenarios, explore multi-threading and parallel processing techniques.

What Rendering Options Should I Configure?

Configure IronPDF for optimal performance:

_renderer.RenderingOptions.EnableJavaScript = false; // If JS not needed
_renderer.RenderingOptions.CssMediaType = PdfCssMediaType.Print;
_renderer.RenderingOptions.RenderDelay = 0; // Remove if no JS
_renderer.RenderingOptions.Timeout = 30; // Set reasonable timeout

For comprehensive rendering configuration options, including viewport settings, custom paper sizes, and page orientation, see the rendering options documentation.

How to Secure Your PDF API?

Security is essential for any production API. Here's a simple API key authentication approach:

// Middleware/ApiKeyMiddleware.cs
public class ApiKeyMiddleware
{
    private readonly RequestDelegate _next;
    private const string ApiKeyHeader = "X-API-Key";
    public ApiKeyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue(ApiKeyHeader, out var apiKey))
        {
            context.Response.StatusCode = 401;
            await context.Response.WriteAsync("API Key required");
            return;
        }
        // Validate API key (in production, check against database)
        var validApiKey = context.RequestServices
            .GetRequiredService<IConfiguration>()["ApiKey"];
        if (apiKey != validApiKey)
        {
            context.Response.StatusCode = 403;
            await context.Response.WriteAsync("Invalid API Key");
            return;
        }
        await _next(context);
    }
}
// In Program.cs
app.UseMiddleware<ApiKeyMiddleware>();

For advanced authentication scenarios, consider:

For PDF-specific security, implement password protection, digital signatures, and PDF sanitization to remove potentially malicious content.

How to Build a Real-World Invoice Generation API?

Let's build a practical invoice generation endpoint demonstrating a complete implementation. This example shows how a production .NET PDF API generates professional invoices with dynamic data.

First Step:
arrow pointer

First, create a new file in your Models folder. Here, I've called mine Invoice.cs. Then add the following code:

public class Invoice
{
    public string InvoiceNumber { get; set; }
    public DateTime Date { get; set; }
    public string CustomerName { get; set; }
    public string CustomerAddress { get; set; }
    public List<InvoiceItem> Items { get; set; }
    public decimal Tax { get; set; }
}
public class InvoiceItem
{
    public string Description { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public decimal Total => Quantity * UnitPrice;
}

Next, create a new service file for the invoice generator. In your Services folder, add the following code. I created a file called InvoiceService.cs. This code handles the styling and layout of your Invoice PDF:

public class InvoiceService
{
    private readonly ChromePdfRenderer _renderer;
    public InvoiceService()
    {
        _renderer = new ChromePdfRenderer();
        _renderer.RenderingOptions.MarginTop = 10;
        _renderer.RenderingOptions.MarginBottom = 10;
        _renderer.RenderingOptions.PrintHtmlBackgrounds = true;
    }
    public byte[] GenerateInvoice(Invoice invoice)
    {
        var html = BuildInvoiceHtml(invoice);
        // Add footer with page numbers
        _renderer.RenderingOptions.HtmlFooter = new HtmlHeaderFooter
        {
            MaxHeight = 15,
            HtmlFragment = "<center><i>{page} of {total-pages}</i></center>",
            DrawDividerLine = true
        };
        var pdf = _renderer.RenderHtmlAsPdf(html);
        return pdf.BinaryData;
    }
    private string BuildInvoiceHtml(Invoice invoice)
    {
        var subtotal = invoice.Items.Sum(i => i.Total);
        var taxAmount = subtotal * (invoice.Tax / 100);
        var total = subtotal + taxAmount;
        var itemsHtml = string.Join("", invoice.Items.Select(item =>
            $@"<tr>
                <td>{item.Description}</td>
                <td class='text-center'>{item.Quantity}</td>
                <td class='text-right'>${item.UnitPrice:F2}</td>
                <td class='text-right'>${item.Total:F2}</td>
            </tr>"));
        return $@"
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body {{ font-family: Arial, sans-serif; }}
                .invoice-header {{
                    background-color: #f8f9fa;
                    padding: 20px;
                    margin-bottom: 20px;
                }}
                table {{
                    width: 100%;
                    border-collapse: collapse;
                }}
                th, td {{
                    padding: 10px;
                    border-bottom: 1px solid #ddd;
                }}
                th {{
                    background-color: #007bff;
                    color: white;
                }}
                .text-right {{ text-align: right; }}
                .text-center {{ text-align: center; }}
                .total-section {{
                    margin-top: 20px;
                    text-align: right;
                }}
            </style>
        </head>
        <body>
            <div class='invoice-header'>
                <h1>Invoice #{invoice.InvoiceNumber}</h1>
                <p>Date: {invoice.Date:yyyy-MM-dd}</p>
            </div>
            <div>
                <h3>Bill To:</h3>
                <p>{invoice.CustomerName}<br/>{invoice.CustomerAddress}</p>
            </div>
            <table>
                <thead>
                    <tr>
                        <th>Description</th>
                        <th>Quantity</th>
                        <th>Unit Price</th>
                        <th>Total</th>
                    </tr>
                </thead>
                <tbody>
                    {itemsHtml}
                </tbody>
            </table>
            <div class='total-section'>
                <p>Subtotal: ${subtotal:F2}</p>
                <p>Tax ({invoice.Tax}%): ${taxAmount:F2}</p>
                <h3>Total: ${total:F2}</h3>
            </div>
        </body>
        </html>";
    }
}

Finally, create a new Controller to access and create invoices using the API:

[ApiController]
[Route("api/[controller]")]
public class InvoiceController : ControllerBase
{
    private readonly InvoiceService _invoiceService;
    public InvoiceController(InvoiceService invoiceService)
    {
        _invoiceService = invoiceService;
    }
    [HttpPost("generate")]
    public IActionResult GenerateInvoice([FromBody] Invoice invoice)
    {
        try
        {
            var pdfBytes = _invoiceService.GenerateInvoice(invoice);
            var fileName = $"Invoice_{invoice.InvoiceNumber}.pdf";
            return File(pdfBytes, "application/pdf", fileName);
        }
        catch (Exception ex)
        {
            return StatusCode(500, $"Error generating invoice: {ex.Message}");
        }
    }
}

For advanced invoice features, consider adding barcodes, QR codes, page numbers, and custom headers/footers. You can also implement PDF/A compliance for long-term archival or integrate with electronic signature workflows.

What Does the Invoice Output Look Like?

PDF invoice document showing Invoice #INV-1023 dated 2025-08-22, with a single line item for 'Misc Object' totaling $495.00 including tax

What Are the Container Deployment Considerations?

While this tutorial focuses on local development, here's a brief overview of containerizing your PDF API:

How Do I Create a Basic Dockerfile?

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["PdfApiService.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# IronPDF requires additional dependencies on Linux
RUN apt-get update && apt-get install -y \
    libgdiplus \
    libc6-dev \
    libx11-dev \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["dotnet", "PdfApiService.dll"]
Text

For detailed deployment guides for your .NET PDF API, see:

What Are the Error Handling Best Practices?

For fault-tolerant programs, implement a global error handler for consistent error responses:

// Middleware/ErrorHandlingMiddleware.cs
public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorHandlingMiddleware> _logger;
    public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred processing request {Path}", context.Request.Path);
            await HandleExceptionAsync(context, ex);
        }
    }
    private static async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = ex switch
        {
            ArgumentNullException => 400,
            UnauthorizedAccessException => 401,
            _ => 500
        };
        var response = new
        {
            error = "An error occurred processing your request",
            message = ex.Message,
            statusCode = context.Response.StatusCode
        };
        await context.Response.WriteAsync(JsonSerializer.Serialize(response));
    }
}

For specific IronPDF troubleshooting scenarios, refer to:

Ready to Build Your Production .NET PDF API?

You've now built a robust .NET PDF API using ASP.NET Core and IronPDF that handles various document generation scenarios. This REST API provides a solid foundation for centralized PDF operations in your applications.

Key takeaways:

  • IronPDF makes PDF generation in Web API projects straightforward with Chrome-based rendering.
  • Easily edit existing PDFs with IronPDF's advanced editing tools.
  • RESTful design principles ensure your PDF API is intuitive and maintainable.
  • Proper error handling and security measures are essential for production.
  • Performance optimization through async operations and caching improves scalability.
  • Full support for desktop and web applications with scalable document solutions.

IronPDF lets developers create PDFs, save PDF files, and convert HTML efficiently, making it the essential PDF API for modern .NET Framework applications.

What Are the Next Steps?

Ready to implement IronPDF in your production .NET PDF API? Here are your next actions:

  1. Start your free trial - Test IronPDF with full functionality in your development environment.
  2. Explore advanced features - Check out digital signatures, PDF forms, PDF/A compliance, metadata management, and other advanced PDF features.
  3. Scale with confidence - Review licensing options for your production API needs, including extensions and upgrades.

Build your .NET PDF API today and streamline document generation across your entire application ecosystem with IronPDF!

Curtis Chau
Technical Writer

Curtis Chau holds a Bachelor’s degree in Computer Science (Carleton University) and specializes in front-end development with expertise in Node.js, TypeScript, JavaScript, and React. Passionate about crafting intuitive and aesthetically pleasing user interfaces, Curtis enjoys working with modern frameworks and creating well-structured, visually appealing manuals.

...
Read More

Related Articles

Key in blue circle

Get your free 30-day Trial Key instantly.

bullet_checkedNo credit card or account creation required
  • Logo Aetna
  • Logo NASA
  • Logo GE
  • Logo Porsche
  • Logo USDA
  • Logo Qatar
Join Millions of Engineers who’ve tried IronPDF
Book your free Live Demo
Booking Badge related to IronPDF Product Demo

Trusted by Millions of Engineers Worldwide

Iron Software's customer logos
Get Your No-Obligation Consult
Complete the form below or email sales@ironsoftware.com
Your details will always be kept confidential.
Trusted by Millions of Engineers Worldwide
Iron Software's customer logos
Get your free 30-day Trial Key instantly.
No credit card or account creation required