Skip to content

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.

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.

from pagr import PagrApiClient
# Simplest form — hosted API, 30s timeout, 2 retries
async with PagrApiClient("pagr_prod_xxxxxxxx") as client:
...
# Full control
async 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 shutdown
client = PagrApiClient("pagr_prod_xxxxxxxx")
try:
...
finally:
await client.aclose() # releases the connection pool

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 budget
result = await client.render(template_id, data, timeout=90)
# A large batch
batch = await client.render_batch(template_id, documents, timeout=300)
# A big PDF over a slow link
pdf = await client.download_document(doc_id, timeout=120)

Every SDK can be told to stop waiting — but each uses its own language’s native primitive rather than an invented Pagr type, and they don’t all cover the same methods.

SDK Primitive Where you can pass it
Python asyncio task cancellation Nowhere — cancelling the awaiting task is enough, so there’s no SDK parameter
TypeScript AbortSignal (signal) render, renderPdf, renderBatch, enqueueBatchRender, getJobStatus, waitForJob, downloadDocument
Java Cancel the CompletableFuture The async client (PagrAsyncApiClient); no per-method parameter
C# CancellationToken Every public async method
Ruby A zero-arg cancelled: predicate wait_for_job only
C++ std::stop_token wait_for_job / wait_for_job_async only

Two rules hold everywhere:

  • A timeout and a cancellation are never confused. The client’s own timeout expiring always raises the SDK’s timeout error; a cancellation you initiated always surfaces as the language’s native cancellation signal, never wrapped in a PagrError. In C++ and Ruby, where there’s no exception to raise, a cancelled wait_for_job instead returns the last polled status — and that status’s done / terminal? is guaranteed false, so you can’t mistake it for a finished job.
  • Cancelling breaks a sleep rather than waiting it out. A cancelled wait_for_job stops on the spot instead of sleeping out the remaining poll interval.

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")

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.

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
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>.
  • Don’t set base_url unless you mean it. It defaults to the hosted API in every SDK. Pointing at the wrong instance is a confusing failure mode — usually surfacing as PagrDecodeError when a proxy or login page answers instead of Pagr.
  • Trailing slashes are trimmed for you, so http://localhost:5110/ and http://localhost:5110 behave identically.
  • max_retries is client-wide, with no per-call override. If one code path needs a different policy, build a second client.
  • max_retries=0 doesn’t disable anything for writes — writes were never retried in the first place.
  • wait_for_job gives up after 5 minutes unless you say otherwise. Omitting the timeout applies a default deadline, not unbounded polling — see the per-SDK table.
  • Probe once at start-up, not per call. get_status is a cheap readiness check; calling it before every render just doubles your request count against the read rate limit.