SDKs
You can call the Pagr API directly over HTTP, but the official SDKs make it easier: they wrap the base URL, attach your API key, parse responses into typed objects, turn HTTP errors into exceptions you can catch, retry transient read failures, and validate list filters client-side. They cover the whole API — templates and versions, every rendering shape, validation, document browsing, fonts, org stats, service probes, and webhook signature verification and payload parsing.
Available languages
Section titled “Available languages”| Language | Package | Client | Sync / async | Notes |
|---|---|---|---|---|
| cURL (raw HTTP) | — | — | — | Call the API directly with any HTTP client — no library to install. See Quickstart. |
| Python | pagr-python (GitHub) |
PagrApiClient |
Async only | Built on httpx, full type hints. The reference implementation — most actively maintained; the others are kept at feature parity with it. |
| TypeScript | pagr (npm) |
PagrApiClient |
Async only (native fetch) |
Zero runtime dependencies, Node.js 18+. Classes parse API responses into iterable result types. |
| Java | final Maven coordinates TBD | PagrApiClient / PagrAsyncApiClient |
Both — a sync client plus an async client returning CompletableFuture |
Gradle library on a Java 21 toolchain. Gson is an api dependency: JsonObject/JsonElement are part of the public surface, so it lands on your compile classpath deliberately. |
| C# / .NET | Pagr.Sdk (NuGet) |
PagrApiClient |
Async only | net8.0, sealed strongly-typed models, Async suffix, CancellationToken throughout, built on System.Text.Json. |
| Ruby | pagr (RubyGems, unpublished) |
Pagr::Client |
Synchronous only | Ruby 3.0+, built on Faraday, idiomatic Ruby naming (see below). |
| C++ | pagr_sdk (CMake package) |
pagr::PagrApiClient |
Both — every call has a sync form and an _async form |
C++20, integrated via CMake, RAII for resource management. |
The Java SDK’s package is still on a placeholder identifier internally — don’t build against a Maven coordinate for it yet; ask us for the current artifact when you request access.
Getting started
Section titled “Getting started”Construct a client with your API key, render one document, and save the PDF:
pip install git+https://github.com/Metanous-BV/pagr-python.gitimport asynciofrom pagr import PagrApiClient
async def main(): async with PagrApiClient("pagr_test_xxxxxxxxxxxxxxxx") as client: result = await client.render( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90", json_data={"Title": "Acme Q3 Invoice", "Amount": 42}, include_document=True, ) if result.ok: result.document.save("out/")
asyncio.run(main())The base URL defaults to the hosted Pagr API — only the key is required.
Everything is async; use await and async with.
npm install pagrimport { PagrApiClient } from 'pagr';
const client = new PagrApiClient('pagr_test_xxxxxxxxxxxxxxxx');
const result = await client.render( '8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90', { Title: 'Acme Q3 Invoice', Amount: 42 }, { includeDocument: true },);
if (result.ok) { await result.document!.save('./out');}Node.js only — save() uses node:fs/promises, so it doesn’t run in a browser bundle.
// build.gradle — final Maven coordinates still TBD, ask us for the current artifactimplementation "<pagr-java-sdk-coordinate>"import org.example.PagrApiClient;import org.example.RenderOptions;import org.example.models.RenderResult;import java.nio.file.Path;import java.util.UUID;
try (PagrApiClient client = new PagrApiClient("pagr_test_xxxxxxxxxxxxxxxx")) {
RenderResult result = client.render( UUID.fromString("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"), "{\"Title\": \"Acme Q3 Invoice\", \"Amount\": 42}", RenderOptions.builder().includeDocument(true).build());
if (result.isOk()) { result.getDocument().save(Path.of("out")); }}A separate PagrAsyncApiClient mirrors every call, returning CompletableFuture<T>.
dotnet add package Pagr.Sdkusing Pagr.Sdk;
using var client = new PagrApiClient("pagr_test_xxxxxxxxxxxxxxxx");
var result = await client.RenderAsync( Guid.Parse("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"), new { Title = "Acme Q3 Invoice", Amount = 42 }, includeDocument: true);
if (result.Ok) await result.Document!.SaveAsync(@"C:\out");The client owns a pooled HttpClient — register it as a singleton, never one per request.
# Gemfile — not yet published; point at the path we share with yougem "pagr", path: "path/to/pagr-ruby-sdk"require "pagr"
client = Pagr::Client.new(ENV.fetch("PAGR_API_KEY"))
result = client.render( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90", { "Title" => "Acme Q3 Invoice", "Amount" => 42 }, include_document: true,)
if result.ok? result.document.save("out")endfind_package(pagr_sdk REQUIRED)target_link_libraries(your_target PRIVATE pagr_sdk::pagr_sdk)#include "pagr/PagrApiClient.hpp"
pagr::PagrApiClient client("pagr_test_xxxxxxxxxxxxxxxx");
auto result = client.render( std::string("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"), R"({"Title": "Acme Q3 Invoice", "Amount": 42})", {.include_document = true});
if (result.ok() && result.document->has_content()) result.document->save("out/");Every method also has an _async form returning std::future<T> — the HTTP client itself
is synchronous, so this runs the call on a worker thread rather than doing true async I/O.
curl -X POST "https://api.pagr.eu/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" \ -H "Authorization: Bearer pagr_test_xxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "documents": [ { "Title": "Acme Q3 Invoice", "Amount": 42 } ], "includeDocument": true }'No SDK required — this is the same call every official SDK below wraps.
Common conventions
Section titled “Common conventions”Whichever language you use, the SDKs share the same behaviour — so what you learn in one carries over:
- Same authentication — a bearer API key passed at construction, swappable at runtime with
set_api_key. See Authentication. - Same error split — protocol failures raise; business outcomes (validation failure, insufficient credit, partial batch) come back as data on the result object.
- Same error mapping — one small exception tree, with transport failures (timeouts, connection errors) folded into it, so you only ever catch one family. See Handle errors and retries.
- Same retry policy — idempotent
GETs retried on500/502/503/504, timeouts and connection errors, with capped exponential backoff and full jitter. Writes never;429never. - Same rendering model — single, synchronous batch and async-job renders, with the same counts,
statusvalues andissueson every result. See Choose a rendering shape. - Same paging —
skip/takewith atotaland a client-side “more pages?” flag. See Listing & pagination. - Same client-side filter validation — an unknown filter field or operator raises immediately instead of silently returning the unfiltered result set.
- Same webhook verification and parsing — one function verifies an incoming callback’s
X-Pagr-Signatureagainst the raw body and then turns it into the right typed progress or completion object, validating the shape. Verification fails by raising, never by returning a boolean you could drop.
Feature matrix
Section titled “Feature matrix”The SDKs are at feature parity: every endpoint, every rendering shape and every safety behaviour is in all six. The two rows below that still differ are deliberate design decisions rather than gaps waiting to be filled.
| Capability | Python | TypeScript | Java | C# | Ruby | C++ |
|---|---|---|---|---|---|---|
| Every endpoint | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Retry policy on reads | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Webhook payload parsing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Webhook signature verification | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Raw-PDF render (render_pdf) |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Per-call timeout override | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Typed list/batch overload for validate |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Client-side filter validation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Filename sanitisation on save() |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Cancel a wait_for_job poll |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cancel an in-flight request | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Sync and async client | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
The two rows that differ are deliberate, not backlog:
- Cancelling an in-flight HTTP request needs a transport-level abort hook that Ruby’s
net_httpadapter and C++’s HTTP client don’t offer. Both still let you cancel await_for_jobpoll (Ruby’scancelled:predicate, C++’sstd::stop_token), which is where waiting actually happens. See Configure the client. - A separate sync and async client is a per-language design choice in Java alone, where the async surface (
PagrAsyncApiClient) is a distinct type rather than an option on one client. C++ and the other four expose a single client — C++’sPagrApiClientjust offers both a sync method and an_asynccounterpart on the same object. See Configure the client.
Naming conventions
Section titled “Naming conventions”Method names follow each language’s conventions rather than Python’s, so the same call reads natively everywhere:
| Operation | Python | TypeScript | Java | C# | Ruby | C++ |
|---|---|---|---|---|---|---|
| Render | render |
render |
render |
RenderAsync |
render |
render |
| Render a raw PDF | render_pdf |
renderPdf |
renderPdf |
RenderPdfAsync |
render_pdf |
render_pdf |
| Render a batch | render_batch |
renderBatch |
renderBatch |
RenderBatchAsync |
render_batch |
render_batch |
| Enqueue async | enqueue_batch_render |
enqueueBatchRender |
enqueueBatchRender |
EnqueueBatchRenderAsync |
enqueue_batch_render |
enqueue_batch_render |
| Poll a job | get_job_status |
getJobStatus |
getJobStatus |
GetJobStatusAsync |
job_status |
get_job_status |
| Validate | validate |
validate |
validate |
ValidateAsync |
validate |
validate |
| List templates | get_templates |
getTemplates |
getTemplates |
GetTemplatesAsync |
templates |
get_templates |
| List documents | get_documents |
getDocuments |
getDocuments |
GetDocumentsAsync |
documents |
get_documents |
| Download a PDF | download_document |
downloadDocument |
downloadDocument |
DownloadDocumentAsync |
download_document |
download_document |
| Org stats | get_org_stats |
getOrgStats |
getOrgStats |
GetOrgStatsAsync |
org_stats |
get_org_stats |
| Verify and parse a callback | parse_signed_callback |
parseSignedCallback |
Webhooks.parseSignedCallback |
WebhookSignature.ParseSignedCallback |
Pagr.parse_signed_callback |
pagr::webhooks::parse_signed_callback |
| Verify a signature only | verify_signature |
verifySignature |
Webhooks.verifySignature |
WebhookSignature.Verify |
Pagr.verify_signature |
pagr::webhooks::verify_signature |
| Parse a callback (already verified) | parse_callback |
parseCallback |
RenderCallback.parse |
RenderCallback.Parse |
Pagr.parse_callback |
pagr::webhooks::parse_callback |
The verify-and-parse form is the one to reach for: it takes the raw request body,
decodes the JSON itself, and so can’t be called in the wrong order or skipped. Note
C# spells the verify-only helper Verify rather than VerifySignature — the class it
lives on is already WebhookSignature.
Note Ruby drops the get_ prefix throughout (client.templates, not client.get_templates) — an accessor doesn’t announce itself as a getter in Ruby.
Related articles
Section titled “Related articles”- Configure the client — base URL, timeouts, retries, key rotation, and client lifetime.
- How the API works — the API the SDKs wrap.
- Choose a rendering shape — all three rendering shapes.
- Handle errors and retries — the exception tree, per language.
- Webhooks — async job callbacks the SDKs verify and parse for you.
