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.
# Page 1 of production documents, newest firstcurl -G "https://pagr-prd-api-public.azurewebsites.net/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://pagr-prd-api-public.azurewebsites.net/v1/documents/$DOC_ID/file" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ --output "$DOC_ID.pdf"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());}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.
