Browse and download documents
Every render made with persist=true (the default) is stored, and you can find it
again later — list it, filter it, and download the PDF. This is how you build a
“my documents” view, re-send a document a user lost, or reconcile what actually
rendered after a batch.
Before you start
Section titled “Before you start”- Documents only exist here if they were rendered with
persist=true. Apersist=falserender is never stored and never appears. - Your organisation’s retention policy may purge the PDF while keeping the metadata. Plan for that — see handling purged PDFs.
How it works
Section titled “How it works”-
List documents with paging, sorting and filters.
The default sort is
renderedAtascending and the default page size is 25. You’ll almost always wantsortDirection: descfor a “most recent first” view. -
Read
totalto know how far you have to page.It’s the full count matching your filters, independent of
skip/take. -
Advance
skipuntil there’s nothing left.Every SDK exposes a “more pages?” flag computed client-side, so the loop needs no extra request.
-
Check
isPdfDeletedbefore downloading.It’s
trueexactly when the download would fail with410 PdfDeleted— so you can filter purged documents out in the listing pass. -
Download by id, not by
viewUrl.viewUrlis a signed, time-limited link meant for a browser. The download endpoint re-authorises every call.
Full example
Section titled “Full example”List the most recent production documents and download the ones whose PDFs are still stored.
import asyncio, osfrom pagr import PagrApiClient
async def main(): os.makedirs("out", exist_ok=True)
async with PagrApiClient("pagr_prod_xxxxxxxx") as client: skip = 0 while True: page = await client.get_documents( skip=skip, take=50, sort_by="renderedAt", sort_direction="desc", filters=[{"field": "environment", "value": "production"}], ) print(f"{skip + len(page.items)}/{page.total}")
for doc in page.items: if doc.is_pdf_deleted: print(f" skipped (purged): {doc.document_name}") continue pdf = await client.download_document(doc.id, timeout=120) with open(f"out/{doc.id}.pdf", "wb") as f: f.write(pdf)
if not page.has_more: break skip += len(page.items)
asyncio.run(main())import { mkdir, writeFile } from 'node:fs/promises';import { PagrApiClient } from 'pagr';
await mkdir('./out', { recursive: true });const client = new PagrApiClient('pagr_prod_xxxxxxxx');
let skip = 0;for (;;) { const page = await client.getDocuments({ skip, take: 50, sortBy: 'renderedAt', sortDirection: 'desc', filters: [{ field: 'environment', value: 'production' }], }); console.log(`${skip + page.items.length}/${page.total}`);
for (const doc of page) { if (doc.isPdfDeleted) { console.log(` skipped (purged): ${doc.documentName}`); continue; } const pdf = await client.downloadDocument(doc.id, { timeoutMs: 120_000 }); await writeFile(`./out/${doc.id}.pdf`, pdf); }
if (!page.hasMore) break; skip += page.items.length;}import org.example.*;import org.example.models.*;import java.nio.file.*;
Files.createDirectories(Path.of("out"));
try (PagrApiClient client = new PagrApiClient("pagr_prod_xxxxxxxx")) { int skip = 0; while (true) { PagedResult<RenderDocument> page = client.getDocuments(ListOptions.builder() .skip(skip) .take(50) .sortBy("renderedAt") .sortDirection(SortDirection.DESCENDING) .filter(new Filter("environment", "production")) .build());
System.out.printf("%d/%d%n", skip + page.getItems().size(), page.getTotal());
for (RenderDocument doc : page.getItems()) { if (doc.isPdfDeleted()) { System.out.println(" skipped (purged): " + doc.getDocumentName()); continue; } byte[] pdf = client.downloadDocument(doc.getId()); Files.write(Path.of("out", doc.getId() + ".pdf"), pdf); }
if (!page.hasMore()) break; skip += page.getItems().size(); }}using Pagr.Sdk;
Directory.CreateDirectory("out");using var client = new PagrApiClient("pagr_prod_xxxxxxxx");
var skip = 0;while (true){ var page = await client.GetDocumentsAsync(new ListOptions { Skip = skip, Take = 50, SortBy = "renderedAt", SortDirection = SortDirection.Descending, Filters = [new Filter("environment", "production")], });
Console.WriteLine($"{skip + page.Items.Count}/{page.Total}");
foreach (var doc in page) { if (doc.IsPdfDeleted) { Console.WriteLine($" skipped (purged): {doc.DocumentName}"); continue; } var pdf = await client.DownloadDocumentAsync( doc.Id, timeout: TimeSpan.FromMinutes(2)); await File.WriteAllBytesAsync(Path.Combine("out", $"{doc.Id}.pdf"), pdf); }
if (!page.HasMore) break; skip += page.Items.Count;}require "fileutils"require "pagr"
FileUtils.mkdir_p("out")client = Pagr::Client.new("pagr_prod_xxxxxxxx")
skip = 0loop do page = client.documents( skip: skip, take: 50, sort_by: "renderedAt", sort_direction: "desc", filters: [{ field: "environment", value: "production" }], ) puts "#{skip + page.items.size}/#{page.total}"
page.each do |doc| if doc.pdf_deleted? puts " skipped (purged): #{doc.document_name}" next end pdf = client.download_document(doc.id, timeout: 120) File.binwrite(File.join("out", "#{doc.id}.pdf"), pdf) end
break unless page.more? skip += page.items.sizeend#include <filesystem>#include <fstream>#include "pagr/PagrApiClient.hpp"
std::filesystem::create_directories("out");pagr::PagrApiClient client("pagr_prod_xxxxxxxx");
int skip = 0;while (true) { pagr::ListOptions options; options.skip = skip; options.take = 50; options.sort_by = "renderedAt"; options.sort_direction = pagr::SortDirection::Descending; options.filters.emplace_back("environment", "production");
const auto page = client.get_documents(options);
for (const auto& doc : page.items) { if (doc.is_pdf_deleted) continue; const auto pdf = client.download_document(doc.id, std::chrono::minutes(2)); std::ofstream out("out/" + doc.id + ".pdf", std::ios::binary); out.write(reinterpret_cast<const char*>(pdf.data()), pdf.size()); }
if (!page.has_more()) break; skip += static_cast<int>(page.items.size());}# Page 1 of production documents, newest firstcurl -G "https://api.pagr.eu/v1/documents" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ --data-urlencode "take=50" \ --data-urlencode "sortBy=renderedAt" \ --data-urlencode "sortDirection=desc" \ --data-urlencode "filters[0].field=environment" \ --data-urlencode "filters[0].value=production"
# Download one (skip any whose isPdfDeleted is true)curl "https://api.pagr.eu/v1/documents/$DOC_ID/file" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ --output "$DOC_ID.pdf"Useful filters
Section titled “Useful filters”The full field/operator tables live in Listing & pagination. These are the ones you’ll reach for most:
| Goal | Filter |
|---|---|
| Only real, unwatermarked output | environment eq production |
| Everything from one template | template.guid eq <templateId> |
| A date range | Two filters together: renderedAt gte <start> and renderedAt lte <end> |
| A specific language variant | language eq fr |
| Documents by name | documentName contains Invoice, or search=Invoice |
| Large documents only | pageCount gte 10 or fileSizeBytes gte 1000000 |
Filters combine with AND. There’s no OR — run separate queries and merge.
Handling purged PDFs
Section titled “Handling purged PDFs”Metadata and PDF have different lifetimes. Retention can remove the file while the record stays, at which point:
- The document still appears in listings and
GET /v1/documents/{id}. isPdfDeletedistrue.GET /v1/documents/{id}/filereturns410 Gonewith codePdfDeleted.
from pagr import ApiError, NotFoundError
try: pdf = await client.download_document(doc_id)except ApiError as exc: if exc.status_code == 410: # code == "PdfDeleted" print("The PDF was purged by retention; metadata remains.") else: raiseexcept NotFoundError: print("No such document.")import { ApiError, NotFoundError } from 'pagr';
try { const pdf = await client.downloadDocument(docId);} catch (err) { if (err instanceof ApiError && err.statusCode === 410) { console.log('The PDF was purged by retention; metadata remains.'); } else if (err instanceof NotFoundError) { console.log('No such document.'); } else throw err;}try { byte[] pdf = client.downloadDocument(docId);} catch (ApiException exc) { if (exc.getStatusCode() == 410) { System.out.println("The PDF was purged by retention; metadata remains."); } else throw exc;} catch (NotFoundException exc) { System.out.println("No such document.");}try{ var pdf = await client.DownloadDocumentAsync(docId);}catch (PagrApiException exc) when (exc.StatusCode == 410){ Console.WriteLine("The PDF was purged by retention; metadata remains.");}catch (PagrNotFoundException){ Console.WriteLine("No such document.");}begin pdf = client.download_document(doc_id)rescue Pagr::ApiError => e raise unless e.status_code == 410 warn "The PDF was purged by retention; metadata remains."rescue Pagr::NotFoundError warn "No such document."endtry { const auto pdf = client.download_document(doc_id);} catch (const pagr::PagrNotFoundException&) { std::cout << "No such document.\n";} catch (const pagr::PagrApiException& exc) { if (exc.status_code() == 410) { std::cout << "The PDF was purged by retention; metadata remains.\n"; } else throw;}Other considerations
Section titled “Other considerations”- Offset paging drifts under concurrent renders. New documents arrive while you
page, shifting the offsets. If exactness matters, add an upper bound —
renderedAtltea timestamp you captured before starting — so the result set can’t grow underneath you. takeis clamped to 1–200, silently. Asking for 5,000 returns 200 with no warning. Readtakeoff the response if you depend on the page size.- Raise the timeout per download, not globally. The client-wide default is 30 seconds, which is fine for listings and tight for a large PDF over a slow link. Every SDK’s download method accepts a per-call override.
- Downloads are reads, so the SDKs retry them. A transient
5xxor connection drop is retried with backoff.410and404are not retried — they’re deterministic. persist=falserenders are invisible here, permanently. There’s no way to recover a non-persisted render; the bytes existed only in that one response.documentTypeis returned but not queryable. You can readTemplate/Invoiceoff each document, but you can’t filter or sort on it.- Test and production documents live in the same list. Filter on
environmentunless you genuinely want both — watermarked test output mixed into a customer-facing list is a common oversight.
Related articles
Section titled “Related articles”- Documents — the endpoints and full field reference.
- Listing & pagination — every filterable field and operator.
- Handle errors and retries — the exception tree, and what gets retried.
- Renders — the workspace view of the same data.
