Validate before rendering
Validation runs the same checks a render would, but produces no document and consumes no credit. Use it wherever a human can still fix the input — a form submission, an import step, a CI check on your payload builder — so the failure arrives before the charge.
Before you start
Section titled “Before you start”- A published template version to validate against.
- An API key. Either environment works: validation applies no environment-specific gate, so test and production keys produce identical results here. (Rendering doesn’t — see the severity gate below.)
How it works
Section titled “How it works”-
Send your data to the validate endpoint.
Same payload shape as a render: an array of documents. The SDKs let you pass a single object and wrap it for you.
-
Read
is_validfor the yes/no answer.It’s
truewhen there are no issues — the same gate a production render applies. See step 4 if you only ever render with test keys. -
Read
errorsandwarningsfor the detail.Each issue carries a
typeyou can switch on, a humandescription, an optionalelement_idpointing at the template element, and adocument_indexfor batches. -
If you only ever render with a test key, check
errorsinstead.Test/preview rendering tolerates
Warning-severity issues (it degrades to a marker or placeholder instead of blocking), sois_validis stricter than it needs to be there.errors.empty?is the narrower, test-mode-accurate check. -
Render only what passed.
Nothing is charged for a validation, so a reject-and-report loop costs only the request.
Full example
Section titled “Full example”Validate, report anything wrong, and render only what’s is_valid — since
is_valid is itself the production gate, that’s exactly the check a
production render needs.
curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90/validate" \ -H "Authorization: Bearer pagr_test_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "documents": [ { "Title": "Acme Q3 Invoice", "Amount": 42 } ] }'
# → { "issues": [] } ⇒ is_valid: true, safe to render anywhere# → { "issues": [ { "severity": "Warning", … } ] }# ⇒ is_valid: false — renders on a test key, BLOCKED on a production keyimport asynciofrom pagr import PagrApiClient
TEMPLATE_ID = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"DATA = {"Title": "Acme Q3 Invoice", "Amount": 42}
async def main(): async with PagrApiClient("pagr_prod_xxxxxxxx") as client: check = await client.validate(TEMPLATE_ID, DATA)
# is_valid is the production gate: false on any Warning or Error. if not check.is_valid: for issue in check.errors + check.warnings: print(f"{issue.severity.value}: {issue.type.value}" f"{f' [{issue.element_id}]' if issue.element_id else ''}" f" — {issue.description}") return
result = await client.render(TEMPLATE_ID, DATA, include_document=True) result.document.save("out/")
asyncio.run(main())import { PagrApiClient } from 'pagr';
const TEMPLATE_ID = '8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90';const DATA = { Title: 'Acme Q3 Invoice', Amount: 42 };
const client = new PagrApiClient('pagr_prod_xxxxxxxx');
const check = await client.validate(TEMPLATE_ID, DATA);
// isValid is the production gate: false on any Warning or Error.if (!check.isValid) { for (const issue of [...check.errors, ...check.warnings]) { console.log(`${issue.severity}: ${issue.type}` + (issue.elementId ? ` [${issue.elementId}]` : '') + ` — ${issue.description}`); }} else { const result = await client.render(TEMPLATE_ID, DATA, { includeDocument: true }); await result.document!.save('./out');}import org.example.PagrApiClient;import org.example.RenderOptions;import org.example.models.*;import java.nio.file.Path;import java.util.*;
UUID templateId = UUID.fromString("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90");Map<String, Object> data = Map.of("Title", "Acme Q3 Invoice", "Amount", 42);
try (PagrApiClient client = new PagrApiClient("pagr_prod_xxxxxxxx")) {
ValidationResponse check = client.validate(templateId, data);
// isValid() is the production gate: false on any Warning or Error. if (!check.isValid()) { List<RenderIssue> blocking = new ArrayList<>(check.getErrors()); blocking.addAll(check.getWarnings()); blocking.forEach(System.out::println); } else { RenderResult result = client.render(templateId, data, RenderOptions.builder().includeDocument(true).build()); result.getDocument().save(Path.of("out")); }}using Pagr.Sdk;
var templateId = Guid.Parse("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90");var data = new { Title = "Acme Q3 Invoice", Amount = 42 };
using var client = new PagrApiClient("pagr_prod_xxxxxxxx");
var check = await client.ValidateAsync(templateId, data);
// IsValid is the production gate: false on any Warning or Error.if (!check.IsValid){ foreach (var issue in check.Errors.Concat(check.Warnings)) Console.WriteLine(issue);}else{ var result = await client.RenderAsync(templateId, data, includeDocument: true); await result.Document!.SaveAsync("out");}require "pagr"
TEMPLATE_ID = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"DATA = { "Title" => "Acme Q3 Invoice", "Amount" => 42 }
client = Pagr::Client.new("pagr_prod_xxxxxxxx")
check = client.validate(TEMPLATE_ID, DATA)
# valid? is the production gate: false on any Warning or Error.if !check.valid? (check.errors + check.warnings).each { |issue| warn issue }else result = client.render(TEMPLATE_ID, DATA, include_document: true) result.document.save("out")end#include "pagr/PagrApiClient.hpp"
const std::string kTemplateId = "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90";const auto data = R"({"Title": "Acme Q3 Invoice", "Amount": 42})";
pagr::PagrApiClient client("pagr_prod_xxxxxxxx");
const auto check = client.validate(kTemplateId, data);
// is_valid() is the production gate: false on any Warning or Error.if (!check.is_valid()) { auto blocking = check.errors(); const auto warnings = check.warnings(); blocking.insert(blocking.end(), warnings.begin(), warnings.end()); for (const auto& issue : blocking) { std::cout << issue.description << "\n"; }} else { const auto result = client.render(kTemplateId, data, {.include_document = true}); result.document->save("out/" + result.document->document_name + ".pdf");}The severity gate
Section titled “The severity gate”is_valid is the production gate: it’s false as soon as any issue is
Warning or Error severity, because production blocks on both.
| Severity | Test / preview render | Production render | is_valid says |
|---|---|---|---|
Information |
renders | renders | valid |
Warning |
renders | blocked | invalid |
Error |
blocked | blocked | invalid |
Read the middle row twice — it runs the other way round from what you’d expect. A
payload with only warnings comes back is_valid: false, but it still renders
fine against a pagr_test_ key; it only fails against a pagr_prod_ key.
Typical culprits are all Warning-severity: MissingBinding,
UnresolvedImage, UnresolvedFont, InvalidColor. These warnings can be highlighted in the test render.
Validating a batch
Section titled “Validating a batch”Pass a list and every issue comes back with the document_index it belongs to, in
one flat array. issues_for(i) slices it per document — and includes batch-wide
issues (document_index is null), so a per-document report never silently drops
a batch-level cause.
documents = [ {"Title": "Acme Q3 Invoice", "Amount": 42}, {"Title": "Acme Q4 Invoice"}, # missing Amount]check = await client.validate(TEMPLATE_ID, documents)
for i, doc in enumerate(documents): issues = check.issues_for(i) print(f"[{i}] {'OK' if not issues else f'{len(issues)} issue(s)'}") for issue in issues: print(" ", issue)
clean = [d for i, d in enumerate(documents) if not check.issues_for(i)]if clean: await client.render_batch(TEMPLATE_ID, clean)const documents = [ { Title: 'Acme Q3 Invoice', Amount: 42 }, { Title: 'Acme Q4 Invoice' }, // missing Amount];const check = await client.validate(TEMPLATE_ID, documents);
documents.forEach((_, i) => { const issues = check.issuesFor(i); console.log(`[${i}] ${issues.length === 0 ? 'OK' : `${issues.length} issue(s)`}`);});
const clean = documents.filter((_, i) => check.issuesFor(i).length === 0);if (clean.length > 0) await client.renderBatch(TEMPLATE_ID, clean);List<Map<String, Object>> documents = List.of( Map.of("Title", "Acme Q3 Invoice", "Amount", 42), Map.of("Title", "Acme Q4 Invoice")); // missing Amount
// validate() has no typed list overload — hand it a JSON array string.ValidationResponse check = client.validate( templateId, new Gson().toJson(documents));
List<Map<String, Object>> clean = new ArrayList<>();for (int i = 0; i < documents.size(); i++) { List<RenderIssue> issues = check.issuesFor(i); System.out.printf("[%d] %s%n", i, issues.isEmpty() ? "OK" : issues.size() + " issue(s)"); if (issues.isEmpty()) clean.add(documents.get(i));}if (!clean.isEmpty()) client.renderBatch(templateId, clean); // renderBatch DOES take a ListNote the asymmetry: renderBatch accepts a List<?>, validate does not.
var documents = new object[]{ new { Title = "Acme Q3 Invoice", Amount = 42 }, new { Title = "Acme Q4 Invoice" }, // missing Amount};
var check = await client.ValidateAsync(templateId, documents);
var clean = documents .Where((_, i) => check.IssuesFor(i).Count == 0) .ToList();
if (clean.Count > 0) await client.RenderBatchAsync(templateId, clean);documents = [ { "Title" => "Acme Q3 Invoice", "Amount" => 42 }, { "Title" => "Acme Q4 Invoice" }, # missing Amount]check = client.validate(TEMPLATE_ID, documents)
documents.each_with_index do |_, i| issues = check.issues_for(i) puts "[#{i}] #{issues.empty? ? 'OK' : "#{issues.size} issue(s)"}"end
clean = documents.select.with_index { |_, i| check.issues_for(i).empty? }client.render_batch(TEMPLATE_ID, clean) if clean.any?const nlohmann::json documents = nlohmann::json::array({ {{"Title", "Acme Q3 Invoice"}, {"Amount", 42}}, {{"Title", "Acme Q4 Invoice"}}, // missing Amount});
const auto check = client.validate(kTemplateId, documents);
for (std::size_t i = 0; i < documents.size(); ++i) { const auto issues = check.issues_for(static_cast<int>(i)); std::cout << "[" << i << "] " << (issues.empty() ? "OK" : "issues") << "\n";}A JSON array is treated as a batch; a single object as a batch of one.
Other considerations
Section titled “Other considerations”- Validation is not a guarantee. It catches data-shape and binding problems,
not everything: a document can still hit
RenderTimeoutorRenderLayoutDegradedat render time, because those depend on how the content actually lays out. Treat validation as a cheap filter, not a contract. - Validate against the version you’ll render. With no
versionargument both calls target the latest published version — which can change under you between the two requests. Pin the same explicitversionon both if the window matters. sampleDatais a free known-good payload. Fetch it from Get a version and validate it to confirm your plumbing works before debugging your own data.- Batch-wide issues have
document_index: null. Don’t filter them out by looking for a matching index — useissues_for(i), which folds them in. severityfails closed. An unrecognised severity string is parsed asErrorby every SDK, so a value a newer server introduces can never let a document through that was meant to be blocked.- Validation costs a write permit, not a render credit — it counts against the render rate-limit category. Validating a 1,000-document batch is one request; a loop of 1,000 single validations is 1,000. See Errors → Rate limits.
Related articles
Section titled “Related articles”- Validate data — the field-level reference.
- Errors → Render issues — every issue type, grouped by severity.
- Render and save a PDF — the render that follows.
- Render a batch — handling partial failure when you skip validation.
- Variables & data — how bindings are defined on the template side.
