Templates
Stored Handlebars documents, so a render sends data rather than markup. List, create, read, update and archive them. Also product-agnostic.
List templates, newest first
https://api.paperpony.dev/v1/templatesParameters
| Name | In | Description |
|---|---|---|
limit | query | |
cursor | query | The `next_cursor` from the previous page. Opaque. |
include_archived | query |
Example
curl https://api.paperpony.dev/v1/templates?limit=20 \
-H "Authorization: Bearer $PAPERPONY_API_KEY"import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
const page = await pony.templates.list({ limit: 20 });
for (const template of page.data) console.log(template.id, template.name);import os
import requests
response = requests.get(
"https://api.paperpony.dev/v1/templates?limit=20",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
)
response.raise_for_status()
print(response.json())Returns
| Field | Type | Description |
|---|---|---|
datarequired | object[] | |
has_morerequired | boolean | |
next_cursorrequired | string | null |
Responses
| Status | Code | Meaning |
|---|---|---|
200 | A page of templates. | |
401 | unauthorized · invalid_api_key | No credential, or the credential is malformed, unknown or revoked. |
422 | invalid_request | The body or the query failed validation. The message names the field. |
429 | rate_limited | Too many requests. Authenticated routes count per account, never per key. |
500 | internal_error | Unexpected failure. Quote request_id when reporting it. |
Create a template
https://api.paperpony.dev/v1/templatesThe source is compiled when it is stored, so a template that could never render is refused here rather than the first time you use it. Only the helpers formatDate, formatCurrency, formatNumber, uppercase and lowercase are available, alongside the block helpers if, unless, each and with.
slug is derived from name when you do not send one.
Body
| Field | Type | Description |
|---|---|---|
namerequired | string | min length 1 · max length 120 |
slug | string | Unique among this account's live templates. Derived from name when omitted.max length 120 |
product | "pdf" | default "pdf" |
sourcerequired | string | min length 1 |
default_options | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
Example
curl https://api.paperpony.dev/v1/templates \
-X POST \
-H "Authorization: Bearer $PAPERPONY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Invoice",
"product": "pdf",
"source": "<h1>Invoice {{number}}</h1>\n<p>{{customer}}</p>\n<p>{{formatCurrency total currency=\"USD\"}}</p>",
"default_options": {
"format": "A4"
}
}'import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
const template = await pony.templates.create({
name: 'Invoice',
product: 'pdf',
source: '<h1>Invoice {{number}}</h1><p>{{customer}}</p>',
default_options: { format: 'A4' },
});
console.log(template.id);import os
import requests
response = requests.post(
"https://api.paperpony.dev/v1/templates",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
json={
"name": "Invoice",
"product": "pdf",
"source": "<h1>Invoice {{number}}</h1>\n<p>{{customer}}</p>\n<p>{{formatCurrency total currency=\"USD\"}}</p>",
"default_options": {
"format": "A4"
}
},
)
response.raise_for_status()
print(response.json())Returns
| Field | Type | Description |
|---|---|---|
idrequired | string | |
productrequired | string | |
namerequired | string | |
slugrequired | string | |
sourcerequired | string | |
engine_versionrequired | string | |
default_optionsrequired | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
created_atrequired | string | date-time |
updated_atrequired | string | date-time |
archived_atrequired | string | null | date-time |
Responses
| Status | Code | Meaning |
|---|---|---|
201 | The template was created. | |
401 | unauthorized · invalid_api_key | No credential, or the credential is malformed, unknown or revoked. |
413 | payload_too_large · page_weight_exceeded | payload_too_large: what you sent is over a limit. page_weight_exceeded: what the document pulled in while rendering is. |
422 | invalid_request | The body or the query failed validation. The message names the field. |
429 | rate_limited | Too many requests. Authenticated routes count per account, never per key. |
500 | internal_error | Unexpected failure. Quote request_id when reporting it. |
Get a template
https://api.paperpony.dev/v1/templates/{id}Archived templates stay readable by id, because a job still refers to the template it was rendered from.
Parameters
| Name | In | Description |
|---|---|---|
idrequired | path |
Example
curl https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB \
-H "Authorization: Bearer $PAPERPONY_API_KEY"import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
const template = await pony.templates.get('tpl_01JZ3M8Q0000000000000000AB');import os
import requests
response = requests.get(
"https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
)
response.raise_for_status()
print(response.json())Returns
| Field | Type | Description |
|---|---|---|
idrequired | string | |
productrequired | string | |
namerequired | string | |
slugrequired | string | |
sourcerequired | string | |
engine_versionrequired | string | |
default_optionsrequired | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
created_atrequired | string | date-time |
updated_atrequired | string | date-time |
archived_atrequired | string | null | date-time |
Responses
| Status | Code | Meaning |
|---|---|---|
200 | The template. | |
401 | unauthorized · invalid_api_key | No credential, or the credential is malformed, unknown or revoked. |
404 | template_not_found | No route matches the path (not_found), or the resource does not exist on this account (template_not_found, job_not_found). |
422 | invalid_request | The body or the query failed validation. The message names the field. |
429 | rate_limited | Too many requests. Authenticated routes count per account, never per key. |
500 | internal_error | Unexpected failure. Quote request_id when reporting it. |
Update a template
https://api.paperpony.dev/v1/templates/{id}Parameters
| Name | In | Description |
|---|---|---|
idrequired | path |
Body
| Field | Type | Description |
|---|---|---|
name | string | min length 1 · max length 120 |
slug | string | max length 120 |
source | string | min length 1 |
default_options | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
Example
curl https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB \
-X PATCH \
-H "Authorization: Bearer $PAPERPONY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Invoice (2026)"
}'import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
await pony.templates.update('tpl_01JZ3M8Q0000000000000000AB', { name: 'Invoice (2026)' });import os
import requests
response = requests.patch(
"https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
json={
"name": "Invoice (2026)"
},
)
response.raise_for_status()
print(response.json())Returns
| Field | Type | Description |
|---|---|---|
idrequired | string | |
productrequired | string | |
namerequired | string | |
slugrequired | string | |
sourcerequired | string | |
engine_versionrequired | string | |
default_optionsrequired | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
created_atrequired | string | date-time |
updated_atrequired | string | date-time |
archived_atrequired | string | null | date-time |
Responses
| Status | Code | Meaning |
|---|---|---|
200 | The updated template. | |
401 | unauthorized · invalid_api_key | No credential, or the credential is malformed, unknown or revoked. |
404 | template_not_found | No route matches the path (not_found), or the resource does not exist on this account (template_not_found, job_not_found). |
413 | payload_too_large · page_weight_exceeded | payload_too_large: what you sent is over a limit. page_weight_exceeded: what the document pulled in while rendering is. |
422 | invalid_request | The body or the query failed validation. The message names the field. |
429 | rate_limited | Too many requests. Authenticated routes count per account, never per key. |
500 | internal_error | Unexpected failure. Quote request_id when reporting it. |
Archive a template
https://api.paperpony.dev/v1/templates/{id}Archives rather than deletes. The template stops being usable for new renders and disappears from the default listing, but stays readable by id so job history keeps making sense.
Parameters
| Name | In | Description |
|---|---|---|
idrequired | path |
Example
Archiving is reversible in the sense that nothing is deleted: rendered jobs keep pointing at the template, and its id stays valid in job history. It stops being usable for new renders.
curl https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB \
-X DELETE \
-H "Authorization: Bearer $PAPERPONY_API_KEY"import { PaperPony } from 'paperpony';
const pony = new PaperPony({ apiKey: process.env.PAPERPONY_API_KEY });
await pony.templates.archive('tpl_01JZ3M8Q0000000000000000AB');import os
import requests
response = requests.delete(
"https://api.paperpony.dev/v1/templates/tpl_01JZ3M8Q0000000000000000AB",
headers={"Authorization": f"Bearer {os.environ['PAPERPONY_API_KEY']}"},
)
response.raise_for_status()
print(response.status_code)Returns
| Field | Type | Description |
|---|---|---|
idrequired | string | |
productrequired | string | |
namerequired | string | |
slugrequired | string | |
sourcerequired | string | |
engine_versionrequired | string | |
default_optionsrequired | object | Every field is optional; the values shown are the defaults. |
format | "A0" | "A1" | "A2" | "A3" | "A4" | "A5" | "A6" | "Letter" | "Legal" | "Tabloid" | "Ledger" | default "A4" |
orientation | "portrait" | "landscape" | default "portrait" |
margin | object | CSS lengths. A bare number is read as pixels. |
print_background | boolean | default true |
scale | number | default 1 · min 0.1 · max 2 |
header_html | string | null | Rendered into the top margin. Templated with the same data as the body. Chromium recognises the classes pageNumber, totalPages, date, title and url inside it. Needs an explicit font-size, and a top margin large enough to hold it.default null |
footer_html | string | null | default null |
page_ranges | string | null | default null |
wait_for | "load" | "domcontentloaded" | "networkidle" | default "networkidle" |
timeout_ms | integer | default 15000 · min 1000 · max 60000 |
filename | string | Sanitized, not rejected: path separators, null bytes and control characters are stripped and the result is capped at 100 characters.default "document.pdf" |
created_atrequired | string | date-time |
updated_atrequired | string | date-time |
archived_atrequired | string | null | date-time |
Responses
| Status | Code | Meaning |
|---|---|---|
200 | The archived template. | |
401 | unauthorized · invalid_api_key | No credential, or the credential is malformed, unknown or revoked. |
404 | template_not_found | No route matches the path (not_found), or the resource does not exist on this account (template_not_found, job_not_found). |
422 | invalid_request | The body or the query failed validation. The message names the field. |
429 | rate_limited | Too many requests. Authenticated routes count per account, never per key. |
500 | internal_error | Unexpected failure. Quote request_id when reporting it. |
X-Request-Id. Quote it if you report a problem.