Template API
Status: Public API contract. Last updated: 2026-06-20T14:32:32-07:00
This document defines the Template Render API. For the lower-level JSON Render API, see
api-reference.en.md. For template authoring (designing, validating, and publishing templates), see the internaltemplate-authoring.en.md— that document is not part of the public docs site.
1. When to use this API
Use the Template Render API when:
- You only want to send a
template_idand businessdataand get back a PDF. - You do not want to handle page sizes, coordinates, element trees, or pagination.
- Multiple systems (ERP, OMS, WMS, billing) need to coordinate around a single document contract.
Use the JSON Render API (api-reference.en.md) instead when:
- You need pixel control over layout, custom report shapes, or interactive designer output.
- The document does not match an existing template.
A simple test:
- “Our team agreed on a template name and the fields we’ll send” → Template Render.
- “Our team is shipping a new layout that doesn’t match anything” → JSON Render.
2. Endpoint
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/template-render |
| Auth | Required — Authorization: Bearer YOUR_TOKEN |
Request Content-Type |
application/json |
| Success | 200, Content-Type: application/pdf |
| Error | 4xx / 5xx, Content-Type: application/json |
curl -X POST "https://api.gpdf.com/api/v1/template-render" \
-H "Authorization: Bearer $GPDF_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Request-Id: $(uuidgen)" \
--data-binary @request.json \
--output out.pdf
This endpoint uses the same public production base URL as the JSON Render API:
https://api.gpdf.com.
Authentication, request ID, error envelope, rate-limit guidance, and limits are identical to the JSON Render API. See api-reference.en.md §2 and §6 for the shared contract.
3. Quick Start
The smallest valid request:
{
"template_id": "shipping_label",
"data": [
{
"recipient_name": "John Doe",
"recipient_address": "123 Main St, Los Angeles, CA",
"sender_name": "Acme Warehouse",
"sender_address": "88 Harbor Rd, Long Beach, CA",
"tracking_number": "TRK1234567890"
}
]
}
curl -X POST "https://api.gpdf.com/api/v1/template-render" \
-H "Authorization: Bearer $GPDF_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @label.json \
--output label.pdf
If you get a PDF back, your token, the route, and the template are all working. Move on to the field reference below.
4. Request fields
{
"template_id": "invoice",
"data": [
{ },
{ }
],
"output": {
"mode": "file",
"file_name": "invoice-001.pdf"
}
}
| Field | Type | Required | Notes |
|---|---|---|---|
template_id |
string |
Yes | Stable template identifier. |
data |
object[] |
Yes | One or more data items. Each item is rendered with the same template; the resulting pages are concatenated into a single PDF. |
output |
object |
No | Output mode and file name. |
4.1 template_id
Stable per-template identifier issued by gPdf. Treat it as opaque.
template_idis what your callers depend on. It does not change when the template’s display name changes.- Display names live in the Console, not in the API.
- Built-in templates use stable semantic IDs such as
invoice,packing_list, andshipping_label. - Custom templates may use generated short-code IDs such as
0q7xmp. Both forms are valid; do not parse them.
4.2 data
Rules:
- Must be a non-empty array.
- Each item must be an object.
- The platform limit on items per request is
10. Exceeding it returnsAPI-002. Split larger batches into multiple requests. - Each item is rendered independently against the template; the page outputs are concatenated in order into one PDF. Useful for batch printing labels, invoices, or statements.
- The template defines which fields are required, which are typed, and how arrays inside an item are structured. The caller’s job is to match that schema.
- Unknown fields are tolerated today (silently ignored). Do not rely on this. A future schema may turn unknown fields into validation errors.
- For
required = truestringfields, an empty string or whitespace-only string is treated as missing. - All items in one request must produce the same global structure (
settings,layers,header,footer). Per-item differences in those globals are rejected withAPI-002. Per-item differences inside the rendered body are fine — that is the whole point.
Multi-item example (3 invoices in one PDF):
{
"template_id": "invoice",
"data": [
{
"invoice_number": "INV-001",
"date_of_issue": "2026-03-11",
"issuer_name": "Acme Cloud Inc.",
"bill_to_name": "Receiver A",
"subtotal": "$100.00", "total": "$100.00", "amount_due": "$100.00",
"items": [
{ "description": "Service A", "qty": 1, "unit_price": "$100.00", "amount": "$100.00" }
]
},
{
"invoice_number": "INV-002",
"date_of_issue": "2026-03-11",
"issuer_name": "Acme Cloud Inc.",
"bill_to_name": "Receiver B",
"subtotal": "$240.00", "total": "$240.00", "amount_due": "$240.00",
"items": [
{ "description": "Service B", "qty": 2, "unit_price": "$120.00", "amount": "$240.00" }
]
},
{
"invoice_number": "INV-003",
"date_of_issue": "2026-03-11",
"issuer_name": "Acme Cloud Inc.",
"bill_to_name": "Receiver C",
"subtotal": "$60.00", "total": "$60.00", "amount_due": "$60.00",
"items": [
{ "description": "Service C", "qty": 1, "unit_price": "$60.00", "amount": "$60.00" }
]
}
]
}
4.3 Inline text markup in string fields
Template Render supports a small, case-sensitive inline markup syntax inside rendered text string values:
| Syntax | Effect |
|---|---|
[B]...[/B] |
Render the enclosed text in bold. |
[L]...[/L] |
Render the enclosed text at 1.2x the effective font size. |
Rules:
- This syntax applies only to regular text and table text output produced by
POST /api/v1/template-render. POST /api/v1/pdf/renderkeeps[B],[/B],[L], and[/L]as literal text; use rich text JSON there.- Tags must be uppercase and fully closed. Unclosed or unmatched tags render as normal characters.
[L]multiplies the effective font size by1.2:10becomes12, and7.5becomes9.0.- Tags can be combined, for example
[B][L]Important[/L][/B]. - Barcode values, barcode text, and watermark text are not parsed as inline markup.
4.4 output
| Field | Type | Default | Notes |
|---|---|---|---|
mode |
"binary" | "file" |
binary |
binary = Content-Disposition: inline; filename="...". file = Content-Disposition: attachment; filename="...". Both return identical PDF bytes. |
file_name |
string |
Auto: gPdf-MMDDHHmmssSSS.pdf |
Sanitised; .pdf is appended automatically. |
Any value other than binary / file for mode returns API-002.
{
"template_id": "invoice",
"data": [ { "invoice_number": "INV-001" } ],
"output": {
"mode": "file",
"file_name": "INV-001-2026-03-11.pdf"
}
}
4.5 Fields the caller does not send
Two field families exist in some internal contexts but must not be sent by callers:
revision_id— gPdf manages template versioning internally. The runtime always resolves the active version for the calling environment.revision_idis not part of the public contract.- Any field starting with
_(e.g._each,_item) — reserved for the template engine itself.
Sending either returns API-002.
5. Templates and environments
A template’s template_id is stable, but availability is scoped to the active
template runtime and the calling token:
- A template can exist but be unavailable if no active runtime has been published.
- A template can exist but be unavailable to your token if it is disabled or scoped to other clients.
Errors you may see during integration:
| Surfaced message | Cause |
|---|---|
Template is not activated in this environment |
The template exists but no version has been published in the current environment. |
Template is disabled in this environment |
The template was explicitly disabled. |
Template authentication required |
The template requires authenticated access and the request is anonymous. |
Client is not allowed to use this template |
The template is scoped to specific clients and your token is not on the list. |
Template runtime artifact not found |
The active version’s artifact is missing. Re-publish the template via the Console. |
These surface as API-002 (or API-101 / API-102 for the auth-related
ones) under the standard error envelope.
6. Discovering available templates
There is no public GET /api/v1/templates endpoint at this time. The
list of templates available to your environment is managed in the gPdf
Console and coordinated out-of-band:
- The team that publishes templates owns the
template_idand field-schema decisions. - Callers receive the contract (template ID + field list + types) through internal documentation, the Console UI, or a shared design system.
This is a deliberate choice for v1. A discovery endpoint may be added later and will be listed in the api-reference.en.md changelog when it is.
7. Built-in example templates
The repository ships three example templates. Whether they are callable in your environment depends on whether they have been published there. Confirm with the team that owns the Console.
7.1 invoice
Top-level fields:
invoice_number,date_of_issue,date_due,billing_periodissuer_name,issuer_address,issuer_phone,issuer_email,issuer_tax_idbill_to_name,bill_to_address,bill_to_emailship_to_name,ship_to_addressamount_due_summary,payment_notesubtotal,total,amount_dueitems(array)
items[] fields:
description,qty,unit_price,amount
Minimum example:
{
"template_id": "invoice",
"data": [
{
"invoice_number": "INV-001",
"date_of_issue": "2026-03-11",
"date_due": "2026-04-10",
"issuer_name": "Acme Cloud Inc.",
"issuer_address": "88 Harbor Rd, Long Beach, CA",
"bill_to_name": "Receiver Inc.",
"bill_to_address": "123 Main St, Los Angeles, CA",
"amount_due_summary": "Amount due on receipt",
"subtotal": "$100.00",
"total": "$100.00",
"amount_due": "$100.00",
"items": [
{
"description": "Service A",
"qty": 1,
"unit_price": "$100.00",
"amount": "$100.00"
}
]
}
]
}
7.2 packing_list
Top-level fields (note the dotted paths):
shipment.number,shipment.date,shipment.vessel,shipment.port_of_loading,shipment.port_of_dischargeshipper.name,shipper.address,shipper.phoneconsignee.name,consignee.address,consignee.phonenotesitems(array)
items[] fields:
item_no,description,quantity,unit,gross_weight,net_weight
Minimum example:
{
"template_id": "packing_list",
"data": [
{
"shipment": {
"number": "PL-001",
"date": "2026-03-11"
},
"shipper": {
"name": "Acme Logistics",
"address": "Shenzhen"
},
"consignee": {
"name": "Receiver Inc.",
"address": "Los Angeles"
},
"items": [
{
"item_no": "1",
"description": "Box A",
"quantity": 10,
"unit": "CTN",
"gross_weight": 20.0,
"net_weight": 18.0
}
]
}
]
}
7.3 shipping_label
Top-level fields:
recipient_name,recipient_address,recipient_company,recipient_phonesender_name,sender_addresstracking_number,weight,pieces,special_requirements
Minimum example:
{
"template_id": "shipping_label",
"data": [
{
"recipient_name": "John Doe",
"recipient_address": "123 Main St, Los Angeles, CA",
"sender_name": "Acme Warehouse",
"sender_address": "88 Harbor Rd, Long Beach, CA",
"tracking_number": "TRK1234567890"
}
]
}
8. Validation rules
Templates run client data through these checks:
8.1 Required fields
A field declared required = true returns API-002 if missing. For string
fields, empty or whitespace-only is treated as missing.
8.2 Field types
The supported field types are:
stringnumberbooleanarray
Other JSON shapes (e.g. nested objects without dotted-path schema declarations) are not allowed unless the template declares them.
8.3 Array items
If a field is array and the template declares an items schema, every
array element is checked against that schema.
For example:
invoice.items[].qtymust be anumber.packing_list.items[].gross_weightandnet_weightmust benumber.
8.4 Dotted paths
Templates can read nested object paths:
shipment.numbershipper.nameconsignee.address
What templates do not support today:
- Numeric array indices in path syntax (e.g.
items[0].qty). - Expressions in paths.
8.5 Unknown fields
Currently, unknown fields in data items do not raise errors. They are
silently dropped.
This is intentional for v1 (forward compatibility), but callers should not rely on it:
- A misspelled field name will silently render an empty value.
- If you set a field but the rendered PDF is unchanged, the most likely cause is a typo or a wrong path.
9. Errors
The error envelope is identical to the JSON Render API. See api-reference.en.md §6.1 for the full table. Common Template-Render-specific triggers:
| Trigger | Code | Typical message |
|---|---|---|
data is not an array |
API-001 |
invalid type |
data is an empty array |
API-002 |
Template render data must contain at least one item |
data exceeds 10 items |
API-002 |
Template render data item count ... exceeds max 10 |
data[n] is not an object |
API-002 |
Template render data[n] must be an object |
| Required field missing | API-002 |
Missing required field <name> |
| Field type mismatch | API-002 |
Field type mismatch |
| Per-item global differs | API-002 |
produced different document globals |
| Page count exceeds policy | API-004 |
page count / max_pages_per_request |
| Template not active or unauthorised | API-002 / API-101 / API-102 |
See §5. |
Sample error response:
{
"error": true,
"code": "API-002",
"message": "Missing required field invoice_number",
"req_id": "7f7d2f5a-4cb0-4c4e-b6ef-8f6d3e0b1fd8"
}
10. Studio-only source authoring metadata
This section is for gPdf Studio and compatible template authoring tools only.
Business callers of POST /api/v1/template-render do not send these fields.
Source templates may include an optional authoring block beside schema and
layout:
{
"source_schema_version": 2,
"source": {
"schema": {
"fields": []
},
"layout": {
"pages": []
},
"authoring": {
"schema_version": 1,
"content_decisions": [
{
"target": {
"path": "/layout/pages/0/elements/0/content",
"kind": "text_content"
},
"decision": "static",
"reason": "keep_as_text",
"content_hash": "sha256:...",
"text_preview": "SHIP TO",
"updated_at": "2026-06-12T17:30:00Z"
}
]
}
}
}
Rules:
authoringis Studio-only metadata. It records template-editor decisions such as “this content has already been reviewed and should stay static”.authoringis not part of the Template Render request contract and must not appear inPOST /api/v1/template-renderpayloads.- Runtime templates,
binding_plan, and PDF rendering ignoreauthoring. It must not affect generated PDF bytes. schemaremains the source of truth for dynamic fields;layoutremains the source of truth for rendered PDF structure.- Existing source templates without
authoringremain valid. Studio should usesource_schema_version: 2when it writesauthoring. - Studio should use
target.path + content_hashto decide whether a static decision is still valid. If the path exists but the hash changed, the content should return to review. content_hashissha256:plus the lowercase hex SHA-256 digest of the UTF-8 bytes of the canonical target content.text_previewis diagnostic only. It should be short and must not be used as the matching key.
content_decisions[] fields:
| Field | Required | Notes |
|---|---|---|
target.path |
Yes | JSON Pointer path relative to source, for example /layout/pages/0/elements/0/content. |
target.kind |
Yes | Target category. Initial supported values: text_content, text_run, table_cell_content, barcode_content, image_asset. |
decision |
Yes | Initial supported value: static. Future authoring tools may add compatible values. |
reason |
No | Authoring reason such as keep_as_text, manual_review, or import_confirmed. |
content_hash |
Yes | Stable hash of the canonical target content, prefixed with sha256:. |
text_preview |
No | Short human-readable preview for Studio UI/debugging. |
updated_at |
No | ISO-8601 timestamp written by the authoring tool. |
Canonical target content:
text_contentandtext_run: the Studio plain-text projection of the text content. Rich/block text uses the same output as StudiotextContentToPlainText; primitive string content uses the string value directly.table_cell_content: the same plain-text projection of the table cell content.barcode_content: the barcodecontentstring.image_asset: the stable static image source identity. For asset references, useasset:plus the trimmed asset key fromassetorsource.kind = "asset"/source.key. For inline base64 image sources, usebase64:plus the normalized image format, a colon, and the trimmed base64 payload. Do not fetch or hash remote image bytes.
11. Integration playbook
When integrating a new template, follow this sequence:
- Smoke-test with a tiny template first.
shipping_labelhas only a handful of required fields. If even that fails, the issue is auth or environment, not the template. - Add output controls next. Verify
output.mode = "file"and a customfile_nameproduce the expectedContent-Disposition. - Then integrate templates with arrays.
invoice.items[]andpacking_list.items[]exercise per-item type checks. - Lock the contract. Pin three things in your team’s design doc:
- The exact production
template_idyou use. - The list of fields and their types.
- Your
output.modepolicy and file-name pattern.
- The exact production
These three lines are the entire contract between systems. Everything else is implementation detail on either side.