Skip to content

.NET client

Typefold.Client is the NuGet package for the Typefold API. It renders reports by name through the Reports API; HTML, template versions and batches through the Renders API; and email and SMS text. It also reads render status, cancels renders and downloads PDFs. The .NET, Python and TypeScript clients have the same features. For templates, assets, webhooks and administration, call the HTTP API.

  • .NET Framework 4.6.2 or later, .NET Core 2.0 or later, or .NET 5 or later.
  • An API key with the render:create, render:read and template:read scopes; see API keys and scopes.
  • A published template to render, such as one named Invoice. See Build your first template.
Terminal window
dotnet add package Typefold.Client

On net8.0 it has no dependencies; on netstandard2.0 it uses System.Text.Json 8.0.6. The package version matches the Typefold release it came with. Use a client no newer than your server; see versioning.

using Typefold.Client;
var client = new TypefoldClient(
new Uri(Environment.GetEnvironmentVariable("PDF_URL")!),
Environment.GetEnvironmentVariable("API_KEY")!);

PDF_URL is your installation’s address, the URL you open Studio at, such as https://pdf.example.com. Create one client per API key and reuse it; it is safe to share between threads.

RenderReportAsync renders the published version of a template, found by name, and returns the file’s bytes. It waits up to 95 seconds for a PDF:

byte[] pdf = await client.RenderReportAsync(new RenderReportRequest
{
Template = TemplateRef.ByName("Invoice"),
Data = new { number = "2026-0042", customer = new { name = "Ada Lovelace" } },
});
await File.WriteAllBytesAsync("invoice.pdf", pdf);

Open invoice.pdf: you should see the template filled with your data, so {{customer.name}} prints Ada Lovelace.

For an Excel file from a spreadsheet template, set Format = ReportFormat.Xlsx. To change the paper, margins or PDF standard for one render, set Pdf.

Register the client as a typed HttpClient, so it shares pooled connections:

builder.Services
.AddHttpClient<TypefoldClient>((http, services) =>
{
var config = services.GetRequiredService<IConfiguration>();
return new TypefoldClient(
new Uri(config["Typefold:BaseUrl"]!),
config["Typefold:ApiKey"]!,
http);
})
.ConfigureHttpClient(http => http.Timeout = TimeSpan.FromSeconds(120));

Then inject TypefoldClient where you need it. Keep the API key in user secrets or environment variables, not in appsettings.json.

RenderAsync renders HTML you send, or a published template version, through the Renders API. It creates the render, waits for it and downloads the PDF.

byte[] pdf = await client.RenderAsync(new DirectRenderRequest
{
Document = new HtmlDocument
{
Html = "<h1>Invoice 42</h1><p>Total: 1,250.00 EUR</p>",
Css = new[] { "h1 { color: #0b5394; }" },
},
Settings = new RenderSettings { PageSize = PageSize.Letter },
});

To render a published template version, pass its ID and the data:

byte[] ticket = await client.RenderAsync(new TemplateVersionRenderRequest
{
TemplateVersionId = versionId,
Data = new { ticket = new { number = "TK-1" } },
});

RenderAsync waits two minutes by default; pass timeout: TimeSpan.FromMinutes(5) to wait five. Without Settings, the page is A4 with 10 mm margins.

For long or numerous renders, create the render, store its ID, and pick up the PDF later:

// 1. Create. Returns as soon as the render is queued.
Render render = await client.CreateRenderAsync(request);
await SaveRenderIdAsync(orderId, render.Id);
// 2. Later, in a job or after a webhook arrives:
render = await client.GetRenderAsync(renderId);
if (render.State == RenderState.Succeeded)
{
byte[] pdf = await client.DownloadAsync(render);
}

To wait in the same process instead, call WaitForRenderAsync(render), which polls until the render finishes. To be called when it finishes, use webhooks. If you no longer need a render, call CancelRenderAsync(render.Id).

CreateBatchAsync creates up to 100 renders in one request; if any item is invalid, none is created. WaitForBatchAsync waits until every item has finished, and DownloadBatchAsync returns one PDF of every item in order:

RenderBatch batch = await client.CreateBatchAsync(
tickets.Select(ticket => new TemplateVersionRenderRequest
{
TemplateVersionId = versionId,
Data = ticket,
}),
idempotencyKey: "tickets-2026-10-12");
batch = await client.WaitForBatchAsync(batch);
if (batch.Counts.Succeeded == batch.Total)
{
byte[] pdf = await client.DownloadBatchAsync(batch.Id);
}

A failed item does not stop the others. batch.Items gives each item’s render Id and State, to download or cancel it as a single render. See Render many documents at once.

RenderReportAsync, CreateRenderAsync, RenderAsync and CreateBatchAsync take an optional idempotencyKey. Retrying with the same key returns what the first call created instead of rendering again, so a network failure never produces a duplicate document:

var key = $"invoice-{invoice.Id}-v{invoice.Revision}";
byte[] pdf = await client.RenderReportAsync(request, idempotencyKey: key);

Use a key that names the document. For reports, keys apply to PDF only. The rules are in idempotency keys.

The client retries 429 RATE_LIMITED for you, up to MaxRateLimitRetries times. Any other API error, or a 429 still refused after those retries, throws TypefoldException; its Error.Code says what went wrong and Error.Retryable whether trying again can help:

try
{
byte[] pdf = await client.RenderReportAsync(request, idempotencyKey: key);
}
catch (TypefoldException ex) when (ex.Error.Code == "RENDER_TIMEOUT")
{
// Still rendering after 95 seconds. Follow it later with GetRenderAsync(ex.Error.RenderId).
await QueueFollowUpAsync(ex.Error.RenderId!);
}
catch (TypefoldException ex) when (ex.Error.Retryable)
{
// RATE_LIMITED, a 5xx, and similar: back off and retry with the same idempotency key.
}
catch (TypefoldException ex)
{
// Not worth retrying as is: fix the request, template or key.
logger.LogError(ex, "PDF render failed: {Code}", ex.Error.Code);
}

Every code is in the error reference.

RenderStringAsync fills a template string and returns the text, with no PDF. See Render email and SMS text.

string body = await client.RenderStringAsync(new RenderStringRequest
{
Template = "{{#if (isEqual Status 'paid')}}Paid{{else}}Due{{/if}}: {{Booking.Code}}",
Data = model,
Engine = StringEngine.Handlebars,
Helpers = "function isEqual(a, b) { return a === b; }",
Format = StringFormat.Html,
});

StringFormat.Html escapes values and StringFormat.Text (the default) does not. Without Engine, only {{path}} placeholders are filled.

Everything is in the Typefold.Client namespace.

new TypefoldClient(baseUrl, apiKey, httpClient):

Option Default Meaning
baseUrl Required The API’s address as a Uri, such as https://pdf.example.com. A base path such as /pdf is kept.
apiKey Required Your API key.
httpClient One with a 120 s timeout A shared HttpClient. A report PDF can take up to 95 seconds, so keep its timeout above that.
MaxRateLimitRetries 3 How many times a 429 is sent again, after its Retry-After (else 1, 2, 4… seconds, at most 30). 0 throws the first one.

MaxRateLimitRetries is a property: new TypefoldClient(baseUrl, apiKey) { MaxRateLimitRetries = 5 }.

Every method also takes a CancellationToken.

Method Returns Scopes
RenderReportAsync(request, idempotencyKey) byte[] render:create, template:read, plus render:read for PDF
RenderStringAsync(request) string render:create
ListReportsAsync() IReadOnlyList<ReportSummary> template:read
GetReportDataSchemaAsync(format, template) JsonElement (JSON Schema) template:read
CreateRenderAsync(request, idempotencyKey) Render render:create
GetRenderAsync(id) Render render:read
WaitForRenderAsync(render, timeout) Render render:read
DownloadAsync(render) byte[] render:read, only when it refreshes the link
RenderAsync(request, idempotencyKey, timeout) byte[] render:create, render:read
CancelRenderAsync(id) Nothing render:cancel
CreateBatchAsync(items, idempotencyKey) RenderBatch render:create
GetBatchAsync(id) RenderBatch render:read
WaitForBatchAsync(batch, timeout) RenderBatch render:read
DownloadBatchAsync(id) byte[] render:read

WaitForRenderAsync polls every 1 to 5 seconds and throws RENDER_TIMEOUT after timeout, two minutes by default; a dropped connection or a retryable error such as RATE_LIMITED does not stop it. WaitForBatchAsync polls the same way, for up to 10 minutes by default. DownloadAsync re-reads the render first when its download link is about to expire, and throws InvalidOperationException for a render that has no file. Cancelling a CancellationToken stops your code waiting, not the render on the server; call CancelRenderAsync for that.

Requests are classes with the API’s fields in PascalCase.

Type Properties
RenderReportRequest Template, Data, Format (pdf), Locale (en-US), Pdf, Image, Test
ReportPdfOptions PageSize, Orientation, Margins, PageRanges, Metadata, Pdfa, Pdfua, OpaqueFills, PreferCssPageSize, Attachments, Encryption, Signature, Operations, Watermark
TemplateVersionRenderRequest TemplateVersionId, Data, Locale, Metadata, Attachments, Encryption, Signature, Watermark, Format, Image, Test, WaitMs
DirectRenderRequest Document (Html, Css, AssetIds), Settings, Metadata, Attachments, Encryption, Signature, Watermark, Format, Image, Test, WaitMs
RenderSettings PageSize (A4), Orientation, Margins (10 mm each, all four required), Locale, PageRanges, Metadata, Pdfa, Pdfua, OpaqueFills, PreferCssPageSize
Margins TopMm, RightMm, BottomMm, LeftMm
RenderStringRequest Template, Data, Format, Engine, Helpers
PdfMetadata Title, Author, Subject, Creator, Keywords, Producer, Language, Xmp
PdfEncryption OpenPassword, OwnerPassword, Permissions (PdfPermissions, with Printing from PdfPrinting)
PdfSignature Certificate, Reason, Location, ContactInfo, Field (PdfSignatureField)
PdfAttachment Name, MimeType, Description, Relationship, Content (text) or ContentBase64 (bytes)
PdfOperation Type (PdfOperationType), Template or Pdf, MergeWholeDocument, MergeToFront, RenderForEveryPage
PdfWatermark Text, Opacity, Rotation, FontSize, Color; see watermarks
ImageOptions Page, Width, Quality, with Format set to RenderFormat.Png, Jpeg or Webp; see images

TemplateRef.ByName, ById and ByShortid pick the template; a shortid exists only on imported templates. PageSize has A3, A4, A5, Letter and Legal, or use new PageSize(widthMm, heightMm). What each field does is in the API reference and PDF standards and security.

Data is sent with its property names unchanged, so new { CustomerName = "Ada" } is {{CustomerName}} in the template, and null values are kept. Anonymous types, records, classes, dictionaries, JsonElement and JsonNode all work. A Newtonsoft JObject works only as the whole Data value; convert a nested one first, for example with JsonNode.Parse(jObject.ToString()). A template version or string render without Data sends {}.

TypefoldException has StatusCode (null when the error did not come from an HTTP response, such as a failed render or a wait that ran out) and Error, the API’s error with Code, Message, Retryable, Details, Diagnostics and RenderId. The client raises these codes itself:

Code When
RENDER_TIMEOUT A wait ran out of time. Retryable; follow RenderId, or the batch for WaitForBatchAsync.
RENDER_FAILED A render was cancelled while RenderAsync waited. A failed render throws with the error it recorded instead.
HTTP_ERROR A response that was not the API’s error format, such as a proxy’s error page or a failed download. Retryable for 429 and 5xx.