Configure the client
The defaults are chosen to be right for most callers, so you can construct a client with nothing but an API key. This page covers the handful of settings worth changing deliberately — and the two lifetime rules that matter in production.
The defaults
Section titled “The defaults”| Setting | Default | Change it when |
|---|---|---|
| Base URL | the hosted Pagr API | You’re targeting another instance (a local dev server, a private deployment). |
| Request timeout | 30 s | A single render or download legitimately takes longer. Prefer a per-call override. |
| Max retries | 2 (3 attempts) on idempotent GETs |
You want retries off (0), or you’re behind a flaky network and want more. |
| API key | — (required) | Rotation, or per-tenant keys in a multi-tenant service. |
Backoff internals — 500 ms first step, 8 s ceiling, 60 s Retry-After ceiling, full
jitter — are the same in every SDK and are not intended as tuning surface. See
Handle errors and retries.
Constructing a client
Section titled “Constructing a client”# There's no client to configure — set the base URL and header per request.export PAGR_BASE_URL="https://pagr-prd-api-public.azurewebsites.net"export PAGR_API_KEY="pagr_prod_xxxxxxxx"
curl "$PAGR_BASE_URL/v1/fonts" -H "Authorization: Bearer $PAGR_API_KEY" \ --max-time 30 --retry 2 --retry-connrefused--max-time and --retry are the rough equivalents of the SDKs’ timeout and
retry settings. Note curl --retry will retry a POST too — the SDKs
deliberately don’t.
from pagr import PagrApiClient
# Simplest form — hosted API, 30s timeout, 2 retriesasync with PagrApiClient("pagr_prod_xxxxxxxx") as client: ...
# Full controlasync with PagrApiClient( api_key="pagr_prod_xxxxxxxx", base_url="http://localhost:5110", # omit for the hosted API timeout=60.0, # seconds max_retries=0, # disable retries) as client: ...
# A long-lived singleton, closed on shutdownclient = PagrApiClient("pagr_prod_xxxxxxxx")try: ...finally: await client.aclose() # releases the connection poolimport { PagrApiClient } from 'pagr';
// Simplest form — hosted API, 30s timeout, 2 retriesconst client = new PagrApiClient('pagr_prod_xxxxxxxx');
// Full controlconst configured = new PagrApiClient( 'pagr_prod_xxxxxxxx', 'http://localhost:5110', // omit (or pass undefined) for the hosted API { timeoutMs: 60_000, maxRetries: 0 },);There is nothing to dispose — a fetch-based client holds no pooled
connection, so this SDK has no context-manager equivalent by design.
import org.example.PagrApiClient;import org.example.internal.PagrClientConfig;import java.time.Duration;
// Simplest form — hosted API, defaultstry (PagrApiClient client = new PagrApiClient("pagr_prod_xxxxxxxx")) { ...}
// Another instancetry (PagrApiClient client = new PagrApiClient("pagr_prod_xxxxxxxx", "http://localhost:5110")) { ...}
// Full control — note builder(apiKey, baseUrl) takes bothPagrClientConfig config = PagrClientConfig .builder("pagr_prod_xxxxxxxx", PagrClientConfig.DEFAULT_BASE_URL) .connectTimeout(Duration.ofSeconds(10)) .requestTimeout(Duration.ofSeconds(60)) .maxRetries(0) .build();
try (PagrApiClient client = new PagrApiClient(config)) { ...}Java is the only SDK that separates connect timeout (default 10 s) from request timeout (default 30 s).
using Pagr.Sdk;
// Simplest form — hosted API, defaultsusing var client = new PagrApiClient("pagr_prod_xxxxxxxx");
// Full controlusing var configured = new PagrApiClient( "pagr_prod_xxxxxxxx", baseUrl: "http://localhost:5110", // pass null for the hosted API options: new PagrClientOptions { Timeout = TimeSpan.FromSeconds(60), MaxRetries = 0, });The client owns a single pooled HttpClient. Register it as a singleton;
never new one per request.
require "pagr"
# Simplest form — hosted API, defaultsclient = Pagr::Client.new("pagr_prod_xxxxxxxx")
# Full controlclient = Pagr::Client.new( "pagr_prod_xxxxxxxx", base_url: "http://localhost:5110", # omit for the hosted API timeout: 60, # seconds max_retries: 0,)
# The block form is scoping sugar, not resource cleanupPagr::Client.new(ENV.fetch("PAGR_API_KEY")) do |c| c.render(template_id, data)endThere is nothing to dispose — Faraday’s connection isn’t a pooled resource tied
to one Client instance, so the block form scopes the variable and nothing more.
#include "pagr/PagrApiClient.hpp"
// Simplest form — hosted API, defaultspagr::PagrApiClient client("pagr_prod_xxxxxxxx");
// Another instance — note the order is (base_url, api_key)pagr::PagrApiClient local("http://localhost:5110", "pagr_prod_xxxxxxxx");
// Full control, via C++20 designated initialiserspagr::PagrApiClient configured({ .base_url = "http://localhost:5110", .api_key = "pagr_prod_xxxxxxxx", .timeout = std::chrono::seconds(60), .max_retries = 0,});Watch the two-argument constructor: it is (base_url, api_key), not
(api_key, base_url). The single-argument form takes the key.
Per-call timeouts
Section titled “Per-call timeouts”This is almost always the right knob, and the one most people miss. The client-wide default of 30 seconds is deliberately tight — it fails fast on a stalled connection — but it’s below the server’s 60-second per-document render budget. So a heavy render or a large download can time out on a request that would have succeeded.
Override it for the specific call rather than inflating the default for everything:
# A heavy render, close to the server's 60s budgetresult = await client.render(template_id, data, timeout=90)
# A large batchbatch = await client.render_batch(template_id, documents, timeout=300)
# A big PDF over a slow linkpdf = await client.download_document(doc_id, timeout=120)const result = await client.render(templateId, data, { timeoutMs: 90_000 });const batch = await client.renderBatch(templateId, documents, { timeoutMs: 300_000 });const pdf = await client.downloadDocument(docId, { timeoutMs: 120_000 });// Java has no per-call timeout override — configure it on the client insteadPagrClientConfig heavy = PagrClientConfig .builder(apiKey, PagrClientConfig.DEFAULT_BASE_URL) .requestTimeout(Duration.ofMinutes(5)) .build();
try (PagrApiClient batchClient = new PagrApiClient(heavy)) { batchClient.renderBatch(templateId, documents);}Use a second, longer-timeout client for the heavy path rather than raising the timeout on the one you use for listings.
var result = await client.RenderAsync( templateId, data, timeout: TimeSpan.FromSeconds(90));
var batch = await client.RenderBatchAsync( templateId, documents, timeout: TimeSpan.FromMinutes(5));
var pdf = await client.DownloadDocumentAsync( docId, timeout: TimeSpan.FromMinutes(2));result = client.render(template_id, data, timeout: 90)batch = client.render_batch(template_id, documents, timeout: 300)pdf = client.download_document(doc_id, timeout: 120)const auto result = client.render( template_id, json_data, {.timeout = std::chrono::seconds(90)});
const auto batch = client.render_batch( template_id, documents, {.timeout = std::chrono::minutes(5)});
const auto pdf = client.download_document(doc_id, std::chrono::minutes(2));Rotating the API key
Section titled “Rotating the API key”Every SDK lets you swap the key on a live client, so you don’t have to tear down a connection pool to rotate a credential — or to serve multiple tenants from one client.
client.set_api_key("pagr_prod_new_key")client.setApiKey('pagr_prod_new_key');client.setApiKey("pagr_prod_new_key");client.SetApiKey("pagr_prod_new_key");client.set_api_key("pagr_prod_new_key") # returns selfclient.set_api_key("pagr_prod_new_key");set_api_key is not thread-safe against in-flight requests in the sense of
ordering: a request already on the wire uses the old key, and there’s no way to know
which key a concurrent request used. Rotate during a quiet moment, or drain first.
Client lifetime
Section titled “Client lifetime”Two rules, and they differ by SDK because the underlying transports differ:
| SDK | Reuse the client? | Dispose? |
|---|---|---|
| Python | Yes — owns an httpx connection pool |
async with, or await client.aclose() on shutdown |
| C# | Yes — critical. Owns a pooled HttpClient |
using, or Dispose() on shutdown |
| Java | Yes | try-with-resources, or close() |
| C++ | Yes | RAII — nothing to call |
| TypeScript | Yes (cheap either way) | Nothing to dispose — no pooled connection |
| Ruby | Yes | Nothing to dispose — the block form is scoping sugar only |
Concurrency and async style
Section titled “Concurrency and async style”| SDK | Style |
|---|---|
| Python | Async only. Every method is a coroutine; use await and async with. |
| TypeScript | Async only, on native fetch. |
| C# | Async only. Every method has an Async suffix and takes a CancellationToken. |
| Java | Both. PagrApiClient is synchronous; PagrAsyncApiClient mirrors every call returning CompletableFuture<T>. |
| Ruby | Synchronous only. |
| C++ | Both. Every method has a sync form and a *_async form returning std::future<T>. |
Other considerations
Section titled “Other considerations”- Don’t set
base_urlunless you mean it. It defaults to the hosted API in every SDK. Pointing at the wrong instance is a confusing failure mode — usually surfacing asPagrDecodeErrorwhen a proxy or login page answers instead of Pagr. - Trailing slashes are trimmed for you, so
http://localhost:5110/andhttp://localhost:5110behave identically. max_retriesis client-wide, with no per-call override. If one code path needs a different policy, build a second client.max_retries=0doesn’t disable anything for writes — writes were never retried in the first place.- Cancellation is C#-only as a first-class concept. Every C# method takes a
CancellationToken. Elsewhere, cancel by tearing down the surrounding task or future. - The Python webhook receiver is optional and dev-only.
LocalWebhookReceiverneeds thewebhookextra (pip install pagr[webhook]) and is meant for local development, examples and tests — not production traffic. - Probe once at start-up, not per call.
get_statusis a cheap readiness check; calling it before every render just doubles your request count against the read rate limit.
Related articles
Section titled “Related articles”- Handle errors and retries — the exception tree and the retry policy in detail.
- Authentication — the bearer key, test vs. production, and keeping it safe.
- SDKs — installation and the per-language feature matrix.
- Service status — the health and version probes.
