Render a raw PDF
The same render endpoint, asked for a different representation. Send
Accept: application/pdf and Pagr streams the PDF binary as the response body
rather than wrapping it in a JSON envelope —
so you skip Base64-decoding a JSON field. The document metadata travels in
X-Pagr-* response headers instead.
Endpoint
Section titled “Endpoint”POST /v1/render/{templateId}POST /v1/render/{templateId}/versions/{version}Identical to Render a document — only the
Accept header differs. The response shape is decided solely by content
negotiation, never by flags or by how many documents happened to render.
Example
Section titled “Example”curl -X POST "https://pagr-prd-api-public.azurewebsites.net/v1/render/8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90" \ -H "Authorization: Bearer pagr_prod_xxxxxxxx" \ -H "Content-Type: application/json" \ -H "Accept: application/pdf" \ -d '{ "documents": [ { "Title": "Acme Q3 Invoice", "Amount": 42 } ] }' \ --output invoice.pdf --dump-header headers.txt--dump-header captures the X-Pagr-* metadata headers alongside the file.
import asynciofrom pagr import PagrApiClient
async def main(): async with PagrApiClient("pagr_prod_xxxxxxxx") as client: result = await client.render_pdf( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90", {"Title": "Acme Q3 Invoice", "Amount": 42}, )
if result.ok: doc = result.document print(doc.document_name, doc.page_count, doc.render_duration) doc.save("out/") # or: pdf_bytes = doc.to_bytes() else: # No PDF to stream — the reasons come back as data, not an exception. print(result.status, result.message) for issue in result.issues: print(issue)
asyncio.run(main())import { PagrApiClient } from 'pagr';
const client = new PagrApiClient('pagr_prod_xxxxxxxx');
const result = await client.renderPdf( '8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90', { Title: 'Acme Q3 Invoice', Amount: 42 },);
if (result.ok) { const doc = result.document!; console.log(doc.documentName, doc.pageCount, doc.renderDuration); await doc.save('./out'); // or: doc.content (Uint8Array)} else { console.log(result.status, result.issues);}// Not available in the Java SDK — see the note below.// Use client.render(...) with RenderOptions.includeDocument(true) and read// the inline bytes off the returned document instead:RenderResult result = client.render( templateId, "{\"Title\": \"Acme Q3 Invoice\", \"Amount\": 42}", RenderOptions.builder().includeDocument(true).build());
if (result.isOk()) { byte[] pdf = result.getDocument().toBytes();}// Not available in the C# SDK — see the note below.// Use RenderAsync with includeDocument: true and read the inline bytes:var result = await client.RenderAsync( templateId, new { Title = "Acme Q3 Invoice", Amount = 42 }, includeDocument: true);
if (result.Ok){ byte[] pdf = result.Document!.ToBytes();}require "pagr"
client = Pagr::Client.new("pagr_prod_xxxxxxxx")
result = client.render_pdf( "8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90", { "Title" => "Acme Q3 Invoice", "Amount" => 42 },)
if result.ok? doc = result.document puts "#{doc.document_name} — #{doc.page_count} page(s)" doc.save("out/") # or: doc.content (String, binary)else warn result.status result.issues.each { |issue| warn issue }end#include "pagr/PagrApiClient.hpp"
pagr::PagrApiClient client("pagr_prod_xxxxxxxx");
auto result = client.render_pdf( std::string("8bec66ff-6f3d-4c1e-9a2b-1f0e5d7c4a90"), R"({"Title": "Acme Q3 Invoice", "Amount": 42})");
if (result.ok()) { auto& doc = *result.document; std::cout << doc.document_name << " — " << doc.page_count << " page(s)\n"; doc.save("out/" + doc.document_name + ".pdf");} else { for (auto& issue : result.issues) { /* handle */ }}A render_pdf_async form returning std::future<PdfRenderResult> is also available.
Path parameters
Section titled “Path parameters”| Parameter | Type | Required | Description |
|---|---|---|---|
templateId |
string (UUID) | Yes | The template to render. |
version |
integer | Only for the specific-version form | The template version number to render. |
Query parameters
Section titled “Query parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
persist |
boolean | true |
Whether to store the rendered document. When false, X-Pagr-Document-Id and X-Pagr-View-Url are omitted. |
language |
string | — | Language variant to render, for multilingual templates. |
Request headers
Section titled “Request headers”| Header | Value | Description |
|---|---|---|
Accept |
application/pdf |
Required to get the raw stream. A wildcard (*/*) does not count — the JSON envelope is the default representation. |
Request body
Section titled “Request body”Exactly the same body as Render a document,
with exactly one entry in documents. includeDocument is ignored — the
bytes are always streamed.
{ "documents": [ { "Title": "Acme Q3 Invoice", "Amount": 42 } ]}Response
Section titled “Response”200 OK with Content-Type: application/pdf — the response body is the PDF.
The filename comes back in Content-Disposition, and the metadata the JSON
envelope would have carried is split across headers:
HTTP/1.1 200 OKContent-Type: application/pdfContent-Disposition: attachment; filename="Acme Q3 Invoice.pdf"X-Pagr-Document-Id: f61aeff4-2c9d-4b7a-8e10-3a5b9c2d1e00X-Pagr-Page-Count: 1X-Pagr-Render-Duration-Ms: 412.7X-Pagr-View-Url: https://…Response headers
Section titled “Response headers”| Header | Description |
|---|---|
Content-Disposition |
attachment; filename="<documentName>.pdf". The SDKs strip the extension so document_name matches the JSON envelope’s no-extension convention. |
X-Pagr-Document-Id |
The stored document id. Omitted when persist=false. |
X-Pagr-Page-Count |
Number of pages. |
X-Pagr-Render-Duration-Ms |
Render time in milliseconds. |
X-Pagr-View-Url |
Signed download URL. Omitted when persist=false. |
X-Pagr-Issue-Count |
Number of non-blocking issues. Omitted when there were none. |
When the render is blocked
Section titled “When the render is blocked”A blocked or failed render has no PDF to stream, so the API does not silently
change shape. It returns 422 Unprocessable Entity with the ordinary JSON
envelope as the body:
{ "status": "failed", "message": "0 document(s) rendered; 1 skipped.", "requestedCount": 1, "renderedCount": 0, "missingCount": 1, "issues": [ { "type": "MissingBinding", "severity": "Error", "description": "…", "documentIndex": 0 } ], "documents": []}The SDKs treat this as a result, not an exception: result.ok is false and
the reasons are in result.status / result.issues. Only genuine protocol
failures raise.
SDK reference
Section titled “SDK reference”| SDK | Method |
|---|---|
| Python | await client.render_pdf(template_id, json_data, *, version=None, language=None, persist=True, timeout=None) → PdfRenderResult |
| TypeScript | await client.renderPdf(templateId, data, options?) → PdfRenderResult |
| Java | — (use client.render(…) with includeDocument(true)) |
| C# | — (use client.RenderAsync(…, includeDocument: true)) |
| Ruby | client.render_pdf(template_id, json_data, version:, language:, persist:, timeout:) → PdfRenderResult |
| C++ | client.render_pdf(template_id, json_data, RenderOptions) / render_pdf_async(…) → PdfRenderResult |
Errors
Section titled “Errors”| Status | When |
|---|---|
406 NotAcceptable |
Accept: application/pdf on a request with more than one document. |
422 |
The render was blocked — body is the JSON envelope (see above), not an error envelope. |
Everything else matches Render a document. See Errors for the full table.
Related articles
Section titled “Related articles”- Render a document — the JSON-envelope form of this endpoint.
- Render and save a PDF — a step-by-step walkthrough of both forms.
- Render statelessly — the other endpoint that returns raw PDF bytes.
- Errors — the full status code reference.
