Skip to content
TryDocify

Guide

Generate PDFs in .NET and ASP.NET Core

Create PDFs from C# with HttpClient and serve them from an ASP.NET Core endpoint, using a typed client and IHttpClientFactory. No native libraries or headless browser.

Updated · Every code sample on this page is run against the API before publishing.

Why an API instead of a library

.NET has good PDF libraries, and TryDocify itself is built on .NET and the QuestPDF layout engine. Calling an API instead of embedding a library means no native rendering dependencies in your deployment, no license to manage in your own codebase, and the same document format from every service, whatever language it is written in. Your code only builds a JSON document and makes one HTTPS request with HttpClient.

Everything below runs on .NET 8 or later, with nothing beyond the base class library: System.Net.Http.Json is built in.

Generate a PDF from C#

Anonymous objects serialize to exactly the JSON the API expects, because PostAsJsonAsync uses camelCase names by default. Set the key in TRYDOCIFY_API_KEY and run:

C# · Program.cs
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("https://api.trydocify.com") };
http.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("TRYDOCIFY_API_KEY"));

var response = await http.PostAsJsonAsync("/api/v1/generate", new
{
    test = true, // free, watermarked render while you build; remove for production

    title = "Quarterly statement",
    blocks = new object[]
    {
        new { type = "paragraph", text = "Account summary for Q3 2026." },
        new
        {
            type = "table",
            columns = new object[] { new { header = "Month" }, new { header = "Balance", align = "right" } },
            rows = new[] { new object[] { "July", 1250.40 }, new object[] { "August", 1310.00 }, new object[] { "September", 1402.75 } },
        },
    },
});

if (!response.IsSuccessStatusCode)
{
    // RFC 9457 problem details: code, title, detail, traceId and (for validation) errors.

    var problem = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
    throw new InvalidOperationException($"{(int)response.StatusCode} {problem?["code"]}: {problem?["detail"]}");
}

await File.WriteAllBytesAsync("statement.pdf", await response.Content.ReadAsByteArrayAsync());
Console.WriteLine($"Saved statement.pdf ({response.Headers.GetValues("X-Page-Count").First()} page)");

A failed request returns a problem document with a stable code such as validation_failed or quota_exceeded. Validation errors include an errors object keyed by JSON path, for example blocks[1].rows[2], so you can see exactly which part of the document to fix. Failed requests never use a credit.

Serve PDFs from ASP.NET Core

In a web app, register a typed client with AddHttpClient so connections are pooled by IHttpClientFactory, and keep the key in configuration (user secrets in development, an environment variable or a vault in production). The endpoint below builds an invoice, renders it and returns it as a download:

C# · Program.cs
// Program.cs (ASP.NET Core minimal API)

using System.Net.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// appsettings.json: { "TryDocify": { "ApiKey": "..." } }, or the TryDocify__ApiKey environment variable.

builder.Services.AddHttpClient<PdfClient>(http =>
{
    http.BaseAddress = new Uri("https://api.trydocify.com");
    http.DefaultRequestHeaders.Add("X-API-Key", builder.Configuration["TryDocify:ApiKey"]);
    http.Timeout = TimeSpan.FromSeconds(30);
});

var app = builder.Build();

app.MapGet("/invoices/{id}/pdf", async (string id, PdfClient pdfs, IHostEnvironment env, CancellationToken ct) =>
{
    var invoice = new InvoiceDocument(
        Test: env.IsDevelopment(), // watermarked and free outside production

        FileName: $"invoice-{id}.pdf",
        Title: $"Invoice {id}",
        Blocks:
        [
            new { type = "keyValue", items = new[] { new { key = "Billed to", value = "Globex Corporation" } } },
            new
            {
                type = "table",
                columns = new object[] { new { header = "Item" }, new { header = "Amount", align = "right" } },
                rows = new[] { new[] { "Consulting (8 h)", "$1,200.00" } },
                footer = new[] { new[] { "Total", "$1,200.00" } },
            },
        ]);

    var pdf = await pdfs.RenderAsync(invoice, ct);
    return Results.File(pdf, "application/pdf", invoice.FileName);
});

app.Run();

public sealed record InvoiceDocument(bool Test, string FileName, string Title, object[] Blocks);

public sealed class PdfClient(HttpClient http, ILogger<PdfClient> logger)
{
    public async Task<byte[]> RenderAsync(InvoiceDocument document, CancellationToken ct)
    {
        using var response = await http.PostAsJsonAsync("/api/v1/generate", document, ct);
        if (!response.IsSuccessStatusCode)
        {
            var problem = await response.Content.ReadAsStringAsync(ct);
            logger.LogError("TryDocify returned {Status}: {Problem}", (int)response.StatusCode, problem);
            response.EnsureSuccessStatusCode();
        }

        return await response.Content.ReadAsByteArrayAsync(ct);
    }
}
  • Test: env.IsDevelopment() gives you free, watermarked PDFs on your machine and real ones in production, with no code change between environments.
  • Results.File sets Content-Type: application/pdf and a Content-Disposition download name. Use Results.File(pdf, "application/pdf") without a name to open it in the browser instead.
  • Pass the request's CancellationToken through, so a closed browser tab cancels the render. A cancelled render is not charged.
  • For large documents, return await response.Content.ReadAsStreamAsync() instead of a byte array to stream the PDF through without buffering it.

Production checklist

  • Add retries for 429 and 503 that honor Retry-After. The Microsoft.Extensions.Http.Resilience package adds them to a typed client with AddStandardResilienceHandler().
  • Set a timeout. Typical documents render in milliseconds, so 30 seconds is generous.
  • Watch your quota with GET /api/v1/usage, or read X-Credits-Remaining from each response.
  • Rotate a leaked key with POST /api/v1/keys/rotate; plan and credits are kept.

New to the document format? Start with the JSON to PDF tutorial, or see a complete, multi-page invoice example.

Try it with your own data

Every plan includes 200 free test renders a day. Add "test": true while you build, and remove it when you go live.