Render API
Status: Public API contract. Last updated: 2026-06-21T02:01:16-07:00
This document defines what callers of the gPdf HTTP API can rely on. It does not describe internal control-plane behaviour, infrastructure, or any field not listed here. Anything not documented here is not part of the public contract and may change without notice.
1. Overview
gPdf turns JSON into PDF at the edge. Start with the smallest JSON Render request in §1.2, then add defaults, metadata, tables, containers, stacks, PDF/A, security, or e-invoice settings only when the document needs them.
This English document is the canonical prose contract for the Render API. The machine-readable contract is the OpenAPI file linked below. Localized docs must derive from this English source and must not introduce independent API meaning.
1.1 API surface and route map
There are three public API families, each tuned for a different integration shape:
| API | Endpoint | When to use |
|---|---|---|
| JSON Render | POST /api/v1/pdf/render |
You describe pages, elements, coordinates, tables, and pagination yourself. Best for designers, custom reports, and one-off layouts. |
| Template Render | POST /api/v1/template-render |
You only send template_id + business data. Best for ERP, OMS, WMS, and any system that wants a stable contract per document type. |
| E-Invoice Render | POST /api/v1/e-invoice/render |
You need a Factur-X / ZUGFeRD compliant PDF/A-3b with embedded CII XML. |
A simple decision tree:
- “I want pixel control over the layout.” → JSON Render
- “My team agreed on a template name and a list of fields.” → Template Render
- “I need an EU-mandate-compliant electronic invoice.” → E-Invoice Render
JSON Render and Template Render return application/pdf on success.
E-Invoice Render returns either application/pdf or an object-delivery job
descriptor, depending on settings.e_invoice.delivery.mode. All three share
the same Bearer-token authentication and the same error-code namespace.
Machine-readable spec: the full API contract is published as an OpenAPI 3.1 document at
/openapi.json(YAML mirror at/openapi.yaml). Use it to generate SDKs (openapi-generator), import into Postman / Insomnia, drive IDE autocomplete, or hand to an AI coding assistant — the OpenAPI is the canonical contract this document narrates. The rootopenapivalue is the OpenAPI Specification version;info.versionis the gPdf public contract document version.
Public route map:
| Route | Auth | Purpose |
|---|---|---|
POST /api/v1/pdf/render |
Authorization: Bearer YOUR_TOKEN |
Primary JSON Render endpoint. |
POST /api/v1/template-render |
Authorization: Bearer YOUR_TOKEN |
Render one or more business records through a published template. |
POST /api/v1/e-invoice/render |
Authorization: Bearer YOUR_TOKEN |
Render a Factur-X / ZUGFeRD e-invoice PDF. |
GET /api/v1/e-invoice/jobs/{job_id} |
Authorization: Bearer YOUR_TOKEN |
Poll an object-delivery e-invoice job. |
GET /api/v1/e-invoice/jobs/{job_id}/artifacts/{artifact} |
Authorization: Bearer YOUR_TOKEN |
Download pdf, xml, request, or report artifacts. |
GET /api/v1/e-invoice/capabilities |
None | Read the public e-invoice capability registry. |
Operational notes:
POST /api/v1/pdf/renderdoes not acceptsettings.e_invoice; e-invoice settings belong only toPOST /api/v1/e-invoice/render.POST /api/v1/e-invoice/renderreturns different content bydelivery.mode:inline_pdfreturns PDF bytes, whileobjectreturns a JSON job descriptor.- Object-mode job lookup and artifact download require a token for the same owner that created the job.
GET /api/v1/e-invoice/capabilitiesis a static product capability registry. It does not guarantee that a specific token is authorized for every listed capability.- JSON parsing, request validation, token policy application, preparation, and rendering are one server-side render flow. They are not separate public endpoints.
1.2 Five-second start: minimum JSON Render request
Many integrations do not need a complete DocumentRequest on day one. If you
only want to prove that JSON can become a PDF, save this as quickstart.json:
{
"pages": [
{
"size": "label_100_150",
"elements": [
{
"type": "text",
"layout": { "left": 10, "top": 18 },
"content": "Hello gPdf"
}
]
}
]
}
Then call the API:
curl -X POST "https://api.gpdf.com/api/v1/pdf/render" \
-H "Authorization: Bearer $GPDF_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Request-Id: quickstart-001" \
--data-binary @quickstart.json \
--output quickstart.pdf
You should get a 100 × 150 mm one-page PDF on disk. If you do not:
| Symptom | Likely cause | Fix |
|---|---|---|
401 with API-101 |
Missing or malformed Authorization header |
Confirm the header is exactly Bearer YOUR_TOKEN. |
403 with API-102 |
Token rejected | Confirm the token is active and valid for the public API. |
400 with API-001 |
Body is not valid JSON | Check the file with jq . or a JSON linter. |
400 with API-002 |
Body parsed but failed validation | Read message — it names the offending field. |
200 with a JSON file on disk |
The API returned an error envelope but the command still used --output |
Drop --output, re-run, and inspect the JSON response. |
The example intentionally omits:
settingsdefaultstext.styleoutput
gPdf fills in the safe style and output defaults needed to produce a PDF. Use the full structure in §3 and §4 when you need metadata, default styles, PDF/A profiles, layers, headers, footers, or pagination behaviour.
The public text element accepts three content forms:
- Plain text shorthand:
"content": "Hello gPdf". - Rich text spans:
"content": { "spans": [...] }. - Block text:
frame+defaults+content.blocks.
All three forms pass through the same validation and rendering pipeline. Block text is the most expressive form for complex layout, variables, list content, and controlled pagination.
1.3 Common default fallbacks
When a request omits a field, gPdf resolves values in this order:
- Element-local fields.
settings.defaults.- Service-level defaults.
The high-frequency cases are:
| Omitted field | Behaviour |
|---|---|
| Text font family | Plain text / spans inherit through text.style.font_family; block text inherits through defaults.run.font_family. If the whole chain is silent, automatic font selection is used. |
| Text font size | Falls back through the same style/default chain to the service default. |
| Text color | Falls back through the same style/default chain to the service default. |
| Text line height | Plain text / spans inherit through text.style.line_height; block text inherits through defaults.paragraph.line_height. |
line.stroke |
Missing stroke width and color are filled from defaults. |
Shape stroke |
rect, circle, ellipse, polygon, and path do not draw an outline unless stroke is declared; partially declared stroke fields still inherit missing subfields. |
fill |
Transparent by default; no fill is drawn unless declared. |
settings.output.mode |
Defaults to binary. |
settings.output.file_name |
A safe file name is generated and .pdf is appended. |
See §4.16 for the full precedence chain.
1.4 Complete request skeleton
Move from the minimum request to the full shape when you need document-wide defaults, metadata, repeated layers, a header/footer, security, PDF/A, or page margins:
{
"settings": {
"defaults": {
"text": {
"font_family": "NotoSans-Regular",
"font_size": 11,
"color": "#111111"
},
"stroke": {
"color": "#000000",
"width": 0.4
},
"fill": {
"color": "#FFFFFF",
"opacity": 1
},
"shape": {
"corner_radius": 0
}
},
"metadata": {
"title": "gPdf Document",
"author": "gPdf"
},
"output": {
"mode": "file",
"file_name": "archive-report-20260310.pdf"
},
"profile": "pdfa-2b"
},
"layers": {
"background": {
"elements": []
},
"watermark": {
"template": {
"type": "text",
"content": "Draft"
},
"layout": {
"preset": "diagonal_tile"
}
},
"stamp": {
"elements": []
}
},
"header": {
"layout": { "height": 12 },
"elements": []
},
"footer": {
"layout": { "height": 10 },
"elements": []
},
"pages": [
{
"size": "label_100_150",
"elements": [
{ "type": "text", "layout": { "left": 8, "top": 24 }, "content": "Full request skeleton" }
]
}
]
}
This skeleton is not required for simple documents. It is a map of the major surfaces that can participate in a production document.
1.5 AI sandbox and trial testing
For AI coding assistants such as Cursor, Copilot, custom GPTs, and developers
who want to quickly test or debug DocumentRequest JSON payloads without
registering or managing API keys, gPdf provides a public sandbox proxy endpoint.
POST /api/playground?endpoint=pdf-render
Host: gpdf.com
Content-Type: application/json
Accept: application/pdf
[!NOTE] Sandbox guidelines and policies:
- No Authorization header required: this trial sandbox automatically binds a secure developer key at the CDN edge. Do not include an
Authorizationheader.- Development and trial only: this endpoint is restricted to local debugging, layout validation, and interactive AI evaluation. It is not for production workloads.
- Fair use: there is no fixed daily sandbox quota at this time, but reasonable fair-use applies. If expected usage exceeds 100 PDF renders per day, register in the gPdf Console and use the paid API endpoint with a live token.
- Rate limits and payload boundaries:
- Rate limit: maximum 60 requests per minute per IP address. Exceeding this returns
429 Too Many Requests.- Max request size: request JSON is limited to 256 KB.
- Output format: the response is an
application/pdfbinary stream.- The sandbox IP rate limit is separate from production token throughput and does not imply production entitlement. On sandbox
429, wait and retry with client-side exponential backoff plus jitter; do not retry immediately in a tight loop.- Registered Free tier: after signing up, Free-tier accounts receive 100 free PDF page credits per day for evaluation with a live token. A one-page PDF consumes one page credit; multi-page PDFs consume one credit per generated page. Free-tier requests may be intentionally paced and are typically about 1 second slower per request than paid plans.
- Commercial use: for production traffic, higher throughput, or advanced features, register in the gPdf Console and use a paid live token.
1.6 Request and response basics
Every request uses:
POST /api/v1/<route>
Host: api.gpdf.com
Content-Type: application/json
Authorization: Bearer YOUR_TOKEN
X-Request-Id: <optional-client-id>
Every successful response carries:
Content-Type: application/pdfContent-Disposition: inline; filename="..."(default) orattachment; filename="..."whenoutput.mode = "file"X-Request-Id: <echoed-or-generated>
Every error response carries:
Content-Type: application/jsonX-Request-Id: <echoed-or-generated>
The error body is always:
{
"error": true,
"code": "API-002",
"message": "x must be >= 0",
"req_id": "7f7d2f5a-4cb0-4c4e-b6ef-8f6d3e0b1fd8"
}
| Field | Type | Notes |
|---|---|---|
error |
boolean |
Always true on the error envelope. |
code |
string |
Public error code. See §6.1. |
message |
string |
Human-readable explanation. Some auth and system errors return a redacted message. |
req_id |
string |
Mirrors X-Request-Id. Always present. Use it when filing support tickets. |
1.7 What this document does not cover
The following are intentionally not part of the public contract:
- Internal storage layout, queue topology, or any specific cloud service used to deliver responses.
- Token issuance, billing, quota provisioning, and policy management. Those happen in the gPdf Console, not over this API.
- Template authoring (how to design and publish a template). See the internal
template-authoring.en.mdif you maintain templates. - Beta endpoints not listed in this document.
2. Authentication and Environments
2.1 Environments
gPdf exposes one public customer-facing API base URL:
| Environment | Base URL | Purpose |
|---|---|---|
| Production | https://api.gpdf.com |
Live traffic. SLAs apply. |
Build your client so the base URL is configuration, not a constant. Most
integrations read it from an environment variable like GPDF_BASE_URL.
Internal debug and test domains are not part of the public API contract and are
intentionally omitted from public docs and OpenAPI.
2.2 Authentication
Every render endpoint requires a Bearer token:
Authorization: Bearer sk_live_<YOUR_API_KEY>
Rules:
- The token is opaque. Do not parse it.
- Tokens are scoped to the account and deployed API surface. A token not valid for the public API is rejected with
API-102. - Tokens may carry policy constraints (max pages per request, allowed PDF/A profiles, allowed e-invoice standards). Constraint violations return
API-002with the offending field named inmessage. - A revoked or expired token returns
API-103with a redacted message. Treat both as “rotate or contact support”.
The single endpoint that does not require authentication is
GET /api/v1/e-invoice/capabilities. See the dedicated
E-invoice API reference.
2.3 Request IDs
Every request gets a request ID. You may supply one via the X-Request-Id
header; if you don’t, gPdf generates one. It is echoed in every response —
both success and error — and is included in error.req_id.
Recommended client behaviour:
- Generate a UUID v4 per outbound request and pass it as
X-Request-Id. - Log the request ID alongside your application’s correlation ID.
- Quote it verbatim when reporting issues.
curl -X POST "https://api.gpdf.com/api/v1/pdf/render" \
-H "Authorization: Bearer $GPDF_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Request-Id: $(uuidgen)" \
--data-binary @request.json \
--output out.pdf
2.4 Rate limiting and retries
gPdf does not currently publish rate-limit headers (X-RateLimit-*,
Retry-After).
gPdf assigns internal usage event identifiers after accepting billable requests. Those identifiers are used for worker-to-xAdmin reporting and queue retry deduplication. Clients do not need to generate usage idempotency keys.
Recommended client behaviour today:
- Cap concurrency per token at a number you negotiated with your gPdf account contact. Most production tokens are sized for sustained 5-20 req/s; bursts above that should be queued client-side.
- On
5xxor network error, retry with exponential backoff (initial 500 ms, max 5 s, max 3 attempts). - On non-throttling
4xx, do not retry. The request will fail the same way every time. Inspectcodeandmessageand fix the request. - Sandbox IP throttling returns HTTP
429without implying subscription quota state. For automated sandbox callers, queue requests and retry with exponential backoff plus jitter, for example start at 1 second and cap at 30 seconds, to avoid parallel retry storms. Authenticated production quota exhaustion returns HTTP429withAPI-203; treat it as an entitlement condition and wait for reset, top up, or upgrade rather than blindly retrying. - For at-most-once semantics on PDF generation, deduplicate on your side before calling. The API does not detect duplicate submissions.
3. Document model and layout choices
The JSON Render request is intentionally small at the top level:
{
"settings": { },
"layers": { },
"header": { "layout": { "height": 12 }, "elements": [] },
"footer": { "layout": { "height": 10 }, "elements": [] },
"pages": [
{
"size": "label_100_150",
"elements": [
{ "type": "text", "layout": { "left": 8, "top": 24 }, "content": "Body content" }
]
}
]
}
Read it as five surfaces:
| Surface | Role |
|---|---|
settings |
Document-wide defaults, metadata, output mode, page margins, PDF/A profile, security, and route-specific e-invoice settings. |
layers |
Repeated background, watermark, and stamp content. |
header / footer |
Page-global decorations with fixed heights. |
pages[] |
Ordered pages. Each page defines size or width/height and its body elements. |
elements[] |
The actual visual objects: text, table, container, barcode, image, shapes, and links. |
3.1 Layout choice: element, table, or container
Choose the smallest layout primitive that matches the document:
| Need | Use |
|---|---|
| A single absolute-positioned object | A normal element in pages[].elements. |
| Rows and columns with headers, row heights, spans, and page splitting | table. |
| A table followed by subtotal, tax, grand total, payment terms, or signature blocks | A body table followed by one or more sibling body container elements. Use layout.flow and layout.gap_after to keep them in source order. |
| A card, address block, status badge, totals block, clipped panel, local default-style scope, or group that should move as one unit | container. |
A published business contract where callers send only template_id + data |
Template Render API, not JSON Render. |
| Factur-X / ZUGFeRD output with embedded CII XML | E-Invoice Render API. |
Default body layout is explicit positioning. Elements placed directly in
pages[].elements keep their own left/top coordinates and do not move down just
because an earlier table, text block, or container became taller. There are two
exceptions to that base rule:
- Use
settings.layout.flowor element-levellayout.flowwhen you want body elements to be planned in source order on the Vertical axis.flowis a body-only planner; it does not affectheader,footer,layers, or watermark content, and it still uses each element’stopas the design coordinate/gap source. - For table-followed-by-content layouts, place the
tableand followingcontainerelements as siblings inpages[].elements; setlayout.flow: trueon each item or enablesettings.layout.flow, and use the table’slayout.gap_afterto create the vertical space before the totals, notes, or signature container.
Important boundaries:
containermay contain normal visual descendants and nestedcontainerelements, but nottable.- Use
containerfor grouped visual elements. Do not put atableinside acontainer; keep the table and the following visual group as sibling body elements. layers,header, andfooterrejecttable.
Use this rule for invoices: line items are a table; subtotal, tax, grand
total, payment instructions, and approval/signature rows are sibling
container elements that follow the table in source order. Use layout.flow
and layout.gap_after to keep the vertical spacing stable when rows paginate
or text wraps.
4. JSON Render API
POST /api/v1/pdf/render is the lower-level rendering endpoint. The request
body is a single DocumentRequest JSON object that describes pages, elements,
styles, and pagination behaviour explicitly. The caller has full control.
If you want a higher-level integration where you only send a template ID and
business data, use POST /api/v1/template-render (see template-api.en.md).
4.1 Endpoint
| Property | Value |
|---|---|
| Method | POST |
| Path | /api/v1/pdf/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/pdf/render" \
-H "Authorization: Bearer $GPDF_TOKEN" \
-H "Content-Type: application/json" \
-H "X-Request-Id: $(uuidgen)" \
--data-binary @request.json \
--output out.pdf
4.2 Request structure
{
"settings": { },
"layers": { },
"header": { },
"footer": { },
"pages": [ ]
}
| Field | Type | Required | Notes |
|---|---|---|---|
settings |
Settings |
No | Global defaults, metadata, output mode, PDF/A profile. See §4.14. |
layers |
Layers |
No | Background / watermark / stamp. See §4.13. |
header |
Section |
No | Global page header. See §4.12. |
footer |
Section |
No | Global page footer. See §4.12. |
pages |
Page[] |
Yes | One or more pages. |
The smallest valid request:
{
"pages": [
{
"size": "label_100_150",
"elements": [
{ "type": "text", "layout": { "left": 8, "top": 20 }, "content": "Hello gPdf" }
]
}
]
}
When you omit settings, gPdf applies safe defaults for fonts, strokes,
fills, and output mode. You do not need to write a 200-line DocumentRequest
to get a usable PDF.
4.3 Page
A page is described by its size and the elements it contains. Body elements
position themselves in millimetres from the page’s top-left corner unless
page_margin is configured.
| Field | Type | Required | Notes |
|---|---|---|---|
size |
string |
One of size or width+height |
Named preset. Case-insensitive. |
width |
number |
One of size or width+height |
Custom page width in mm. Must be between 10 and 2000. |
height |
number |
One of size or width+height |
Custom page height in mm. Must be between 10 and 2000. |
margin |
PageMargin |
No | Per-page margin override. |
elements |
Element[] |
No | Body elements. May be empty. |
Rules:
sizeandwidth/heightare mutually exclusive on the same page. Providing both returnsAPI-002.- A page without
sizemust provide bothwidthandheight. - Custom
width/heightvalues are in millimetres and are limited to10..=2000per side. This covers standard paper, labels, engineering drawings, and common posters while catching common unit mistakes such as inches or pixels submitted as millimetres.
4.3.1 Size presets
| Preset | Dimensions | Typical use |
|---|---|---|
a4 |
210 × 297 mm |
Default office page in most non-US locales. |
a6 |
105 × 148 mm |
Postcards, small notices. |
letter |
215.9 × 279.4 mm |
US default. |
legal |
215.9 × 355.6 mm |
US legal documents. |
label_100_100 |
100 × 100 mm |
Square labels. |
label_100_150 |
100 × 150 mm |
Most common shipping label. |
label_4_6_in |
101.6 × 152.4 mm |
US 4×6“ shipping label. |
{
"pages": [
{
"size": "a4",
"elements": [
{ "type": "text", "content": "A4 preset", "layout": { "left": 10, "top": 14 } }
]
},
{
"size": "letter",
"elements": [
{ "type": "text", "content": "Letter preset", "layout": { "left": 10, "top": 14 } }
]
},
{
"width": 100,
"height": 150,
"elements": [
{ "type": "text", "content": "100 x 150 mm custom page", "layout": { "left": 8, "top": 14 } }
]
}
]
}
4.3.2 Page margin and content box
Configure margins globally for the whole PDF (settings.layout.page_margin) or override
selected edges per page (pages[].layout.page_margin):
{
"settings": {
"layout": {
"page_margin": { "top": 10, "right": 12, "bottom": 10, "left": 12 },
"gap_after": 4
}
},
"pages": [
{
"size": "label_100_150",
"layout": {
"page_margin": { "top": 8, "right": 10, "bottom": 12, "left": 10 }
},
"elements": [
{
"type": "text",
"content": "Content box origin",
"layout": { "left": 0, "top": 0 }
}
]
}
]
}
Behaviour:
- Without
settings.layout.page_marginorpages[].layout.page_margin, body elements use absolute page coordinates and bodylayout.leftvalues must not be negative. - Once
settings.layout.page_marginorpages[].layout.page_marginis set, body elementlayout.left/layout.topbecome relative to the content box (the area inside the margins). - Body horizontal positioning is content-box relative when page margins are active:
layout.left: 0starts at the effective left margin; negativelayout.leftmay move content toward the paper edge but must not be less than-effective_left_margin; right-side content may enter the right margin but must not pass the physical page edge. - Elements that exceed the allowed horizontal page bounds or vertical content box return
API-002. There is no automatic clipping. headerandfooteruse section-local vertical coordinates. Explicitlayout.leftvalues are margin-relative whensettings.layout.page_marginorpages[].layout.page_marginis active: rendered left = effective left margin +layout.left. Negative sectionlayout.leftvalues may move content toward the paper edge, but they must not be less than-effective_left_margin; without margins, negative sectionlayout.leftvalues are invalid.layout.anchorkeeps normal anchor semantics. Layers remain physical-page coordinates.- Auto-paginated overflow continues from the top of the next page’s content box.
- Without page margins, auto-paginated overflow is rebased to
header.layout.height + settings.layout.pagination.continuation_top_gap_with_headerwhen a header exists, otherwise tosettings.layout.pagination.continuation_top_gap.
Common content boxes with 15mm margins:
| Page size | Page size (mm) | Content box (mm) |
|---|---|---|
| A4 | 210 x 297 |
180 x 267 |
| Letter | 215.9 x 279.4 |
185.9 x 249.4 |
| Legal | 215.9 x 355.6 |
185.9 x 325.6 |
4.4 Coordinates and units
- All coordinates and lengths are in millimetres (mm).
- The origin is the top-left corner of the page (or the content box if
settings.layout.page_margin/pages[].layout.page_marginis set). - The Horizontal axis goes right; the Vertical axis goes down.
- Layout tip: without page margins,
layout.left: 0andlayout.top: 0start at the physical page edge. This is valid for full-page backgrounds, edge-aligned decoration, or bleed-like elements, but regular visible content such as text, tables, barcodes, and business graphics should keep at least5mmfrom physical page edges for print safety. Prefersettings.layout.page_marginfor document-level spacing, usually15mmfor business documents, or set positivelayout.left/topvalues when no page margin is configured. - See §6.5 for
rotationrules per element.
4.5 Element types
The pages[].elements array, header.elements, footer.elements,
layers.background.elements, and layers.stamp.elements contain element
objects. Every element has a type discriminator.
type |
Section | Notes |
|---|---|---|
text |
§4.6 | Plain, rich spans, or block text. |
barcode |
§4.7 | 2D matrix and 1D linear formats. |
image |
§4.8 | Asset reference or inline base64. |
line |
§4.9 | Single line segment. |
rect |
§4.9 | Rectangle, optionally rounded. |
circle |
§4.9 | Circle by centre + radius. |
ellipse |
§4.9 | Ellipse by centre + radii. |
polygon |
§4.9 | Closed polygon from a point list. |
path |
§4.9 | Structured native vector path. |
link |
§4.9 | Standalone clickable hotspot. |
container |
§4.9 | Geometric group for cards, badges, clipped panels, and nested groups. |
table |
§4.10 | Tabular data with headers, spans, pagination. |
Headers and footers accept the full element union, including table, for
repeated page furniture. layers.background, layers.stamp, and container
descendants use the lightweight subset: text, barcode, image, line,
rect, circle, ellipse, polygon, path, link, and container.
table is not allowed in layers or container descendants.
Parent-child hierarchy summary:
| Parent | Allowed children | Forbidden children / notes |
|---|---|---|
pages[].elements |
Full element union: text, barcode, image, line, rect, circle, ellipse, polygon, path, link, container, table |
Top-level body flow. |
header.elements, footer.elements |
Full element union, including table |
Use for repeated page furniture, not for body line-item tables or table-followed-by-content flows. |
layers.background.elements, layers.stamp.elements |
Lightweight subset: text, barcode, image, line, rect, circle, ellipse, polygon, path, link, container |
No table. layers.watermark uses the separate watermark contract. |
container.elements |
Lightweight subset, including nested container |
No table; place a body table and a following body container as siblings for table-followed-by-content layout. |
Common fields shared by most elements:
| Field | Type | Notes |
|---|---|---|
layout |
ElementLayout, TableElementLayout, or ContainerElementLayout |
Positioning and flow intent. Ordinary box elements hold left/top/right/bottom/anchor, flow, gap_after, and z_index; table uses a narrower left/top/flow/gap_after/z_index subset; container adds children. |
comment |
string |
Free-form note. Not rendered. |
rotation |
number |
See per-element rules. Only elements whose section lists rotation accept it; table and container do not. text accepts arbitrary integer angles, image and path accept arbitrary numeric angles, and barcode/rect/ellipse use constrained renderer semantics. |
link |
LinkSpec |
Make the element clickable. See §4.9.7. |
Hyperlink modes:
- Attach
linkto an element (text,barcode,line,rect,circle,ellipse,polygon,path,image,container). - Use a standalone
type: "link"hotspot when you need to overlay a clickable region.
4.5.1 Horizontal anchor (anchor)
layout.left, layout.right, and layout.anchor are the three horizontal positioning forms for
box-like elements. left is the default left-edge coordinate. right positions
the element by distance from the current content right edge. anchor aligns an
element relative to a named reference edge. They are supported on text,
barcode, rect, path, image, link, and container. They are
not supported on line, circle, ellipse, polygon, table,
or block-text content.
{
"type": "text",
"content": "$1,235.85",
"style": {
"width": 24,
"text_align": "right"
},
"layout": {
"anchor": { "reference": "content_right", "offset": 8 },
"top": 12
}
}
Normal references for ordinary positioned elements:
| Reference | Meaning |
|---|---|
page_left |
Left page edge. |
page_right |
Right page edge. |
content_left |
Left edge of the content box. Falls back to the page edge if no margin is set. |
content_right |
Right edge of the content box. |
Resolved horizontal position:
- Left references:
resolved_left = reference + offset - Right references:
resolved_left = reference - offset - element_width right:resolved_left = content_right - right - element_width
Rules:
left,right, andanchorare mutually exclusive. Sending more than one returnsAPI-002.- When using
rightoranchor, the element must have a width:text(plain orspansshorthand):style.widthtext(block):frame.widthbarcode,rect,path,image,link,container: their ownwidthfield.
Vertical positioning:
topis the default top-edge coordinate.bottomis supported on the same box-like elements asright; it positions the element by distance from the current content bottom edge:resolved_top = content_bottom - bottom - element_height.topandbottomare mutually exclusive. Use one vertical positioning source per element.- For text,
bottomrequires an explicitstyle.heightor blockframe.height. - For barcode, rect, path, image, link, and container,
bottomuses the elementheight. tabledoes not supportrightorbottom; keep using its normalleft/topand flow semantics.
4.6 Text
The text element accepts three input forms:
| Form | Use when |
|---|---|
| Plain text shorthand | One short string in one style. |
spans rich text |
One paragraph mixing multiple inline styles. |
| Block text | Multi-paragraph layout, lists, page breaks, variables, or pagination control. |
All three pass through the same validation and rendering pipeline. Block text is the most expressive.
Required fields for any form:
layout.toporlayout.bottom(number).content(string,{ spans }, or{ blocks }).- One of
layout.left,layout.right, orlayout.anchor.
4.6.1 Plain text
{
"type": "text",
"layout": { "left": 18, "top": 18 },
"content": "Hello gPdf"
}
With styling:
{
"type": "text",
"layout": { "left": 18, "top": 18 },
"content": "Invoice #INV-2026-001",
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 12,
"font_weight": "bold",
"color": "#111827",
"width": 90,
"text_align": "left",
"line_height": 1.25,
"letter_spacing": 0.2
}
}
4.6.2 spans rich text
spans is a single paragraph composed of style runs. Each span inherits
the element-level style and overrides only what it sets — typical use
cases are price lines, badges, and inline footnote markers.
{
"type": "text",
"layout": { "left": 18, "top": 30 },
"content": {
"spans": [
{ "text": "Total ", "style": { "color": "#6b7280" } },
{ "text": "USD ", "style": { "color": "#6b7280", "font_size": 9 } },
{ "text": "1,248.50",
"style": { "font_weight": "bold", "font_size": 14, "color": "#111827" } },
{ "text": " (incl. tax)", "style": { "color": "#6b7280" } },
{ "text": "*",
"style": { "script": "superscript", "color": "#dc2626" } }
]
},
"style": {
"font_family": "NotoSans-Regular",
"font_size": 11,
"width": 90
}
}
4.6.3 Block text
Block text uses an explicit document tree of blocks and inlines. The
example below stitches together every common feature in one element — a
heading paragraph, a justified body paragraph mixing bold / coloured /
italic runs, an ordered list whose items each carry their own bold
emphasis, and a right-aligned footer paragraph that prints page / total_pages. The frame uses overflow: "paginate" so the article flows
across as many pages as it needs.
Text content shape summary:
content = string
| { "spans": [ { "text": "...", "style": { ... }, "link": { ... } } ] }
| { "blocks": [
{ "type": "paragraph", "style": { ... }, "inlines": [ ... ] },
{ "type": "list", "list": { ... }, "items": [ ... ] },
{ "type": "page_break" }
] }
paragraph.inlines[] = { "type": "text", "text": "..." }
| { "type": "variable", "name": "page", "scope": "system" }
| { "type": "line_break" }
| { "type": "tab" }
Plain string text, span text, and table-cell text can use {page} and
{total_pages}. Do not write ${page} or ${total_pages}; the dollar sign
is literal while the variable still resolves separately. For structured block
text, prefer inline variable nodes such as
{ "type": "variable", "name": "page", "scope": "system" }.
Profile restrictions still apply: table-cell text and barcode_text allow
only paragraph blocks and do not allow tabs or inline links.
{
"type": "text",
"layout": { "left": 8, "top": 20 },
"frame": {
"width": 84,
"overflow": "paginate"
},
"defaults": {
"run": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8.5,
"color": "#1f2937"
},
"paragraph": {
"align": "left",
"direction": "auto",
"line_height": 1.45
}
},
"content": {
"blocks": [
{
"type": "paragraph",
"inlines": [
{
"type": "text",
"text": "Quarterly Review",
"style": { "font_size": 12, "font_weight": "bold", "color": "#111827" }
}
]
},
{
"type": "paragraph",
"style": { "align": "justify" },
"inlines": [
{ "type": "text", "text": "Revenue grew " },
{ "type": "text", "text": "23% year-over-year",
"style": { "font_weight": "bold", "color": "#0f766e" } },
{ "type": "text", "text": " on the back of three drivers — enterprise expansion, the new self-serve tier, and improved retention. The next two quarters will focus on the " },
{ "type": "text", "text": "EMEA rollout",
"style": { "font_style": "italic" } },
{ "type": "text", "text": "." }
]
},
{
"type": "list",
"list": { "kind": "ordered" },
"items": [
{ "blocks": [{ "type": "paragraph", "inlines": [
{ "type": "text", "text": "Enterprise pipeline doubled to " },
{ "type": "text", "text": "$48M qualified ARR",
"style": { "font_weight": "bold" } },
{ "type": "text", "text": "." }
] }] },
{ "blocks": [{ "type": "paragraph", "inlines": [
{ "type": "text", "text": "Self-serve activation reached " },
{ "type": "text", "text": "61%", "style": { "font_weight": "bold" } },
{ "type": "text", "text": " — three points above target." }
] }] },
{ "blocks": [{ "type": "paragraph", "inlines": [
{ "type": "text", "text": "Net retention held at " },
{ "type": "text", "text": "118%", "style": { "font_weight": "bold" } },
{ "type": "text", "text": ", a record for the company." }
] }] }
]
},
{
"type": "paragraph",
"style": { "align": "right" },
"inlines": [
{ "type": "text", "text": "Page " },
{ "type": "variable", "name": "page", "scope": "system" },
{ "type": "text", "text": " / " },
{ "type": "variable", "name": "total_pages", "scope": "system" }
]
}
]
}
}
Restriction: a single
textelement cannot mix an explicitpage_breakblock withsystem.pageorsystem.total_pagesvariables. Page numbers are evaluated before pagination, so the renderer rejects the combination at validation time. Useframe.overflow = "paginate"to break across pages when content overflows (as above), or usepage_breakblocks with static text only.
Top-level fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type |
"text" |
Yes | |
layout.top or layout.bottom |
number |
One of | See §4.5.1. |
layout.left, layout.right, or layout.anchor |
One of | See §4.5.1. | |
rotation |
integer |
No | Any integer angle. |
layout.z_index |
number |
No | |
comment |
string |
No | |
link |
LinkSpec |
No | Element-level link. Mutually exclusive with any inline link. |
style |
TextStyle |
No | For plain or spans form. |
frame |
BlockTextFrame |
No | For block form. |
defaults |
BlockTextDefaults |
No | Default run / paragraph / frame for the block tree. |
content |
string | { spans } | { blocks } |
Yes | The text content in one of the three forms. |
Paragraph block
{
"type": "paragraph",
"style": {
"align": "justify",
"direction": "auto",
"line_height": 1.35
},
"inlines": [
{ "type": "text", "text": "Line 1: " },
{ "type": "variable", "name": "page", "scope": "system" }
]
}
paragraph.style fields:
- Currently rendered (validated and applied at runtime):
align:left | center | right | justifydirection:auto | ltr | rtlline_height(number)
- Reserved in the schema but rejected by validation today. Do not send
these fields in runnable requests; the validator returns
paragraph layout features … not wiredso callers get an explicit error instead of silent layout differences:space_before,space_after(mm)indent_left,indent_right,indent_first_line,hanging_indent(mm)keep_together,keep_with_next(boolean)widow_orphan_control(boolean)tabs(array)
List block
{
"type": "list",
"list": { "kind": "bullet" },
"items": [
{
"blocks": [
{
"type": "paragraph",
"inlines": [
{ "type": "text", "text": "First item — " },
{ "type": "text", "text": "with bold emphasis",
"style": { "font_weight": "bold" } }
]
}
]
},
{
"blocks": [
{
"type": "paragraph",
"inlines": [{ "type": "text", "text": "Second item, first paragraph." }]
},
{
"type": "paragraph",
"inlines": [{ "type": "text", "text": "An item may carry multiple paragraphs." }]
}
]
},
{
"blocks": [
{
"type": "paragraph",
"inlines": [{ "type": "text", "text": "Third item." }]
}
]
}
]
}
list.list accepts only kind (ordered | bullet) at runtime today.
Spacing / numbering controls (marker_gap, item_spacing, start_at,
continuation rules) are reserved in the schema but rejected by validation
today. Do not send them in runnable requests; the validator returns
list spacing/continuation features … not wired so callers get an explicit
error instead of silent layout differences.
list is only allowed in the full text profile (see §4.6.4). It is
rejected inside header, footer, layers, table cells, and barcode text.
Page break
{ "type": "page_break" }
page_break is the only way to force a page break inside block text. The
older \f string convention is no longer supported.
Inline nodes
Four inline node types are public:
| Type | Example |
|---|---|
text |
{ "type": "text", "text": "Hello", "style": { "font_weight": "bold" } } |
variable |
{ "type": "variable", "name": "page", "scope": "system" } |
line_break |
{ "type": "line_break" } |
tab |
{ "type": "tab" } |
variable.scope may be:
system— page numbers and totals. JSON Render only resolvespageandtotal_pages.binding— values supplied by the template-data pipeline. Validation fails in JSON Render. Use Template Render for data substitution.computed— derived values. Validation fails in JSON Render today.
Inline text style
run-level fields:
font_family,font_sizefont_weight:normal | medium | semibold | boldfont_style:normal | italicfont_mode:strict | prefercolor,opacity,letter_spacingscript:normal | superscript | subscripthighlight,decoration,link_style
Plain TextStyle is the compatibility style object used by simple
text.style, table.cell.text, columns[].cell.text,
columns[].header_cell.text, cell.style.text, and barcode_text.style. It
contains three groups of fields:
| Group | Fields | Notes |
|---|---|---|
| Run style | font_family, font_size, font_weight, font_style, font_mode, color, opacity, letter_spacing, script, highlight, decoration, link_style, wrap_policy |
Visual run-level styling. |
| Paragraph shorthand | text_align, direction, line_height |
Converted into paragraph defaults for compatibility text. |
| Frame-like shorthand | width, height, vertical_align, text_overflow, shrink_to_fit, min_font_size |
Converted into a shadow frame for validation. Rejected in table-cell text and barcode_text.style. |
Rules:
font_modecannot appear without a same-levelfont_family.font_mode = "auto"is not a public input. Auto mode is implicit when no font is declared anywhere in the inheritance chain.strictfailure (declared font cannot cover the text) returnsAPI-002.autoorprefertotal fallback failure returnsAPI-504.highlightis the inline text-run background. It acceptscolor,opacity,corner_radius, and logicalpadding.wrap_policyis aTextStylefield only. Legal values arenormal,keep_together, andno_wrap. It is valid in plainTextStylelocations such astext.style,table.cell.text,columns[].cell.text,columns[].header_cell.text,cell.style.text, andbarcode_text.style. Do not put it inParagraphStyle,InlineTextStyle, table column width modes, or path watermark styles.- Do not write
style.alignin simple text, table-cell text,barcode_text.style, or anyTextStylelocation. Usestyle.text_alignthere. The field namealignbelongs only to blockParagraphStyle, such asdefaults.paragraph.alignor paragraph blockstyle.align.
Block text frame
frame fields:
width,height(mm)vertical_align:top | middle | bottomoverflow:visible | clip | ellipsis | paginateshrink_to_fit(boolean)min_font_size(number)padding,stroke,fillcolumns,column_gap
Rules:
frameis rejected inside table cells and barcode text.rotation != 0cannot combine withframe.overflow = "paginate".frame.heightcannot combine withpage_break.frame.overflow ∈ { clip, ellipsis }cannot combine withpage_break.frame.overflow = "paginate"cannot combine withshrink_to_fit = true.header/footer/ layer text cannot usepaginateor multi-column.
4.6.4 Text feature profiles
The exact subset of text features available depends on where the text
appears. There is no profile field in the JSON; the profile is implied by
container.
| Profile | Used in | Allowed blocks | Allowed inlines |
|---|---|---|---|
| Full | pages[].elements[] (top-level body text) |
paragraph, list, page_break |
text, variable, line_break, tab |
| Section | header, footer, layers.background, layers.stamp |
paragraph |
text, variable, line_break, tab |
| Table | Inside table cells |
paragraph |
text, variable, line_break |
| Barcode | Inside barcode_text |
paragraph |
text, variable, line_break |
layers.watermark does not use the normal text element profiles. It uses the
separate WatermarkLayer contract: the public template type is currently
text, layout is controlled by layout.preset, and validation follows the
watermark-specific rules.
All three restricted profiles reject list and page_break. Table and
barcode profiles additionally reject frame, tab, and inline link. The
section profile keeps frame available but with two carve-outs:
frame.overflow = "paginate" and multi-column (frame.columns > 1) are not
allowed inside a header / footer / layer (the parent section is not itself a
paginating context).
Frame fields enter validation through the plain text.style shorthand too —
style.width / style.height / style.vertical_align / style.text_overflow
/ style.shrink_to_fit / style.min_font_size are coerced into a shadow
frame for contract checking. Setting any of these on a barcode_text.style
or table-cell text style therefore produces the same frame is not allowed
rejection as a literal frame: { ... } block.
4.7 Barcode
2D / matrix code (square module grid):
{
"type": "barcode",
"layout": { "left": 18, "top": 34 },
"format": "qrcode",
"content": "https://gpdf.com/docs/api-reference/#47-barcode",
"width": 28,
"height": 28,
"style": {
"color": "#111111",
"fill": { "color": "#FFFFFF" }
},
"barcode_text": {
"enabled": true,
"position": "bottom",
"offset": 1.5,
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8,
"color": "#374151",
"text_align": "center"
}
}
}
1D / linear code (rectangular bars). The same shape works for any format
in the linear group below — only format and content change:
{
"type": "barcode",
"layout": { "left": 18, "top": 70 },
"format": "code128",
"content": "INV-2026-001",
"width": 60,
"height": 16,
"style": {
"color": "#000000",
"fill": { "color": "#FFFFFF" }
},
"barcode_text": {
"enabled": true,
"position": "bottom",
"offset": 1.0,
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8,
"color": "#111111",
"text_align": "center"
}
}
}
Required fields:
layout.toporlayout.bottom, plusformat,content,width,height.- One of
layout.left,layout.right, orlayout.anchor.
Optional: style, options, barcode_text, rotation, layout.z_index,
comment, link.
Rules:
- For stable output, use only
0,90,180, or270. The current PDF renderer treats other integer angles as0. barcode_textinherits the same rotation.formatis case-insensitive.-and_are equivalent separators.- 2D / matrix codes encode as a module matrix; 1D / linear codes encode as bars;
maxicodeuses a hexagonal grid.
Supported format values (current build):
- 2D / matrix:
qrcode(qr),microqr(micro-qr),pdf417,micropdf417,datamatrix(data-matrix),gs1datamatrix,aztec,maxicode,gs1qrcode - 1D / linear:
code128,code128a,code128b,code128c,gs1128,code39,code93,codabar,ean8,ean13,upca,upce,itf(interleaved2of5),itf14,gtin8,gtin12,gtin13,gtin14,isbn,sscc - Other:
msi,msi10,msi11,msi1010,msi1110,upus10,uspsimb,upcacomposite,upcecomposite
4.8 Image
{
"type": "image",
"layout": { "left": 4, "top": 8 },
"width": 33,
"height": 11,
"asset": "aitrack-1",
"format": "jpg"
}
Required fields:
layout.toporlayout.bottom, pluswidth,height.- One of
layout.left,layout.right, orlayout.anchor.
Optional: rotation, layout.z_index, comment, link.
Image source — exactly one of:
- Shorthand: top-level
asset(with optional top-levelformat). - Explicit: top-level
sourceobject.
{ "type": "image", "layout": { "left": 4, "top": 8 }, "width": 33, "height": 11,
"source": { "kind": "asset", "key": "aitrack-1", "format": "jpg" } }
{ "type": "image", "layout": { "left": 4, "top": 8 }, "width": 33, "height": 11,
"source": { "kind": "base64", "format": "jpg",
"payload": "/9j/4AAQSkZJRgABAQAASABIAAD/4QBARXhpZgAATU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAABSqADAAQAAAABAAAAbgAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/+IH2ElDQ19QUk9GSUxFAAEBAAAHyGFwcGwCIAAAbW50clJHQiBYWVogB9kAAgAZAAsAGgALYWNzcEFQUEwAAAAAYXBwbAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALZGVzYwAAAQgAAABvZHNjbQAAAXgAAAWKY3BydAAABwQAAAA4d3RwdAAABzwAAAAUclhZWgAAB1AAAAAUZ1hZWgAAB2QAAAAUYlhZWgAAB3gAAAAUclRSQwAAB4wAAAAOY2hhZAAAB5wAAAAsYlRSQwAAB4wAAAAOZ1RSQwAAB4wAAAAOZGVzYwAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAFEdlbmVyaWMgUkdCIFByb2ZpbGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAHwAAAAxza1NLAAAAKAAAAYRkYURLAAAAJAAAAaxjYUVTAAAAJAAAAdB2aVZOAAAAJAAAAfRwdEJSAAAAJgAAAhh1a1VBAAAAKgAAAj5mckZVAAAAKAAAAmhodUhVAAAAKAAAApB6aFRXAAAAEgAAArhrb0tSAAAAFgAAAspuYk5PAAAAJgAAAuBjc0NaAAAAIgAAAwZoZUlMAAAAHgAAAyhyb1JPAAAAJAAAA0ZkZURFAAAALAAAA2ppdElUAAAAKAAAA5ZzdlNFAAAAJgAAAuB6aENOAAAAEgAAA75qYUpQAAAAGgAAA9BlbEdSAAAAIgAAA+pwdFBPAAAAJgAABAxubE5MAAAAKAAABDJlc0VTAAAAJgAABAx0aFRIAAAAJAAABFp0clRSAAAAIgAABH5maUZJAAAAKAAABKBockhSAAAAKAAABMhwbFBMAAAALAAABPBydVJVAAAAIgAABRxlblVTAAAAJgAABT5hckVHAAAAJgAABWQAVgFhAGUAbwBiAGUAYwBuAP0AIABSAEcAQgAgAHAAcgBvAGYAaQBsAEcAZQBuAGUAcgBlAGwAIABSAEcAQgAtAHAAcgBvAGYAaQBsAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDoAHIAaQBjAEMepQB1ACAAaADsAG4AaAAgAFIARwBCACAAQwBoAHUAbgBnAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8EFwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCAMEAbAB0AGEAbADhAG4AbwBzACAAUgBHAEIAIABwAHIAbwBmAGkAbJAadSgAUgBHAEKCcl9pY8+P8Md8vBgAIABSAEcAQgAg1QS4XNMMx3wARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkAbABPAGIAZQBjAG4A/QAgAFIARwBCACAAcAByAG8AZgBpAGwF5AXoBdUF5AXZBdwAIABSAEcAQgAgBdsF3AXcBdkAUAByAG8AZgBpAGwAIABSAEcAQgAgAGcAZQBuAGUAcgBpAGMAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAAUgBHAEIALQBQAHIAbwBmAGkAbABQAHIAbwBmAGkAbABvACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjAG9mbpAaAFIARwBCY8+P8GWHTvZOAIIsACAAUgBHAEIAIDDXMO0w1TChMKQw6wOTA7UDvQO5A7oDzAAgA8ADwQO/A8YDrwO7ACAAUgBHAEIAUABlAHIAZgBpAGwAIABSAEcAQgAgAGcAZQBuAOkAcgBpAGMAbwBBAGwAZwBlAG0AZQBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBlAGwOQg4bDiMORA4fDiUOTAAgAFIARwBCACAOFw4xDkgOJw5EDhsARwBlAG4AZQBsACAAUgBHAEIAIABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABSAEcAQgAtAHAAcgBvAGYAaQBpAGwAaQBHAGUAbgBlAHIAaQENAGsAaQAgAFIARwBCACAAcAByAG8AZgBpAGwAVQBuAGkAdwBlAHIAcwBhAGwAbgB5ACAAcAByAG8AZgBpAGwAIABSAEcAQgQeBDEESQQ4BDkAIAQ/BEAEPgREBDgEOwRMACAAUgBHAEIARwBlAG4AZQByAGkAYwAgAFIARwBCACAAUAByAG8AZgBpAGwAZQZFBkQGQQAgBioGOQYxBkoGQQAgAFIARwBCACAGJwZEBjkGJwZFAAB0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRNAAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBs/8AAEQgAbgFKAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAFf/aAAwDAQACEQMRAD8A/vwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAK+OP2ov2+v2WP2PLQn43+I3t79o/Mi0zT7S41K/kB6Ygto5GGexfavvX2PQMA5A61UbX95aCd7aH8lH7Qv8AwdFT6PJcaV+y3+z/AOK9cYZWLUPEcMthCTzg/Z4I5pGHTgyIfpX40fGT/g4P/wCC1HxPmnh8JacPAlpNkLFonhuSSRFPT97eJcNkeox9K/0b9xo3HrXZDE0Y7Ul83c55Uaj3n+B/lq+F/GP/AAXU/b68RyeGPDOrfFLxdJuxMiXF3YWMO/8A56NmC3jX2Ygegr9APhf/AMGt/wDwUi+MZi1j4/8Ai/QPCSv8zpf30+s3qg/7MQMWfUef+Nf6FRYmkrSWZz2pxSJWDjvNtn8dvw4/4NA/gbp0cc3xY+MWualL8pkj0jTrexTPcBpWuGI9DgV93fDr/g1+/wCCXPgTUrXWNTtPFHiO4tJY50/tLWCqb4mDDKW0cIIJHIOQRxX9ElFc8sbXlvM0WGpLaJ8iftO/sF/siftm3Wg3n7T3giz8YHwwJ10xLySZY7cXOzzMJFIitu8tfvA9OO9ea+Fv+CT/APwTS8F3CXXh74F+DI5ExhpdJgnPHr5qvn8a/QWisVVmlZSdvU1cIt3aOH074ZfDbR/CNl8P9J8PaZa6DpoVbTTYrSJLS3CZ2iOELsQDJxtUYzXXWVjY6bbraabBHbxL91IlCKPoBgVaoqLsoKKKKQBRRRQAUUUUAFFFFABRRRQAUUUUAf/Z" } }
Rules:
assetandsourceare mutually exclusive.source.kindacceptsassetandbase64.- The
payloadforbase64is the raw base64 content without adata:image/...;base64,prefix. Data URIs are rejected. - Supported formats:
jpg,jpeg,png,webp,svg. rotationaccepts any integer angle (e.g.45,-30).- See §6.2 for image and total request body size limits.
4.9 Shapes and links
Shape elements share the same stroke, fill, link, and stacking rules. Each
may attach an optional link: LinkSpec.
4.9.1 Line
{
"type": "line",
"x1": 4, "y1": 99,
"x2": 96, "y2": 99,
"stroke": {
"color": "#000000",
"width": 0.4,
"dash": { "preset": "solid" }
}
}
Stroke fields fall back through settings.defaults.stroke → service defaults
when omitted (see §4.16).
4.9.2 Rect
{
"type": "rect",
"layout": {
"anchor": { "reference": "content_right", "offset": 6 },
"top": 20
},
"width": 60,
"height": 20,
"fill": { "color": "#FFFFFF" },
"stroke": { "color": "#222222", "width": 0.6 },
"corner_radius": 2
}
4.9.3 Circle
{
"type": "circle",
"cx": 40, "cy": 40, "r": 12,
"fill": { "color": "#E6F4FF" },
"stroke": { "color": "#2B6CB0", "width": 0.5 }
}
4.9.4 Ellipse
{
"type": "ellipse",
"cx": 70, "cy": 40, "rx": 16, "ry": 10,
"rotation": 0,
"fill": { "color": "#FFF7E6" },
"stroke": { "color": "#C05621", "width": 0.5 }
}
4.9.5 Polygon
points[].x and points[].y are geometry-only point coordinates for the
polygon shape. They are not element placement fields; positioned elements use
layout.left/layout.top, layout.right/layout.bottom, or layout.anchor.
{
"type": "polygon",
"points": [
{ "x": 20, "y": 80 },
{ "x": 35, "y": 60 },
{ "x": 50, "y": 80 },
{ "x": 40, "y": 95 }
],
"fill": { "color": "#F0FFF4" },
"stroke": { "color": "#2F855A", "width": 0.5 }
}
4.9.6 Path
path is a controlled native vector outline element. Callers send SVG path
data in d; the renderer does not accept SVG DOM, <path> XML, CSS style,
transform, or external resources.
{
"type": "path",
"layout": {
"anchor": { "reference": "content_right", "offset": 10 },
"top": 210
},
"width": 50,
"height": 18,
"view_box": { "x": 0, "y": 0, "width": 500, "height": 180 },
"d": "M 12 90 C 80 20 160 170 240 80 Q 360 10 488 92",
"stroke": {
"color": "#111111",
"width": 0.6,
"cap": "round",
"join": "round"
}
}
Rules:
- Use one of
layout.left,layout.right, orlayout.anchor;widthandheightare page-space millimetres. view_boxdefines the local coordinate system ford; its geometry fields remainx,y,width, andheight, and itswidthandheightmust be positive. These are geometry-only fields, not element placement fields.dsupports SVG path data lettersM/L/H/V/Q/C/S/T/A/Zand lowercase relative variants.dmust start withM/m, expand to at most 1024 normalized segments, and contain at least one drawing segment.- Coordinates and control points must stay within
view_box; enlargeview_boxwhen control points need to extend farther. stroke.widthis in page-space millimetres and is not scaled byview_box.stroke.compoundis not supported.- Attached
linkregions use the resolvedleft/top/width/heightbounding box.
4.9.7 Link spec and standalone link
A LinkSpec is reused everywhere a hyperlink is allowed:
{
"target": { "type": "url", "url": "https://gpdf.com/docs/api-reference/#497-link-spec-and-standalone-link" },
"alt": "Open the official site",
"padding": 1.0,
"border": { "color": "#1A202C", "width": 0.3 }
}
LinkTarget variants:
- URL:
{ "type": "url", "url": "https://..." } - In-document jump:
{ "type": "page", "page": 2, "left": 10, "top": 20 }
Rules:
- URL schemes allowed:
http://,https://,mailto:,tel:. Others returnAPI-002. - URL strings are trimmed before being written to the PDF annotation.
pageis 1-indexed and must not exceed the request’s page count.paddingandborder.widthmust be finite and>= 0.border.colormust be a valid hex colour if provided.
Standalone link element:
{
"type": "link",
"layout": {
"anchor": { "reference": "content_left", "offset": 10 },
"top": 10
},
"width": 40,
"height": 8,
"target": { "type": "url", "url": "https://gpdf.com/docs/api-reference/#497-link-spec-and-standalone-link" },
"alt": "Open website",
"border": { "color": "#2563EB", "width": 0.4 }
}
Use the standalone form for clickable region overlays not bound to a specific element.
4.9.8 Container
container is a first-class geometric grouping element. Use it when a visual
group should move, edit, inherit descendant defaults, clip, or paginate as one
unit. It is designed for cards, address blocks, status badges, totals blocks,
bordered groups, clipped panels, and nested groups.
It is not an HTML/CSS container and does not accept DOM, flexbox,
percentage sizing, auto margins, grow/shrink fields, children, background,
border, radius, row, column, rotation, or y_anchor aliases.
Top-level fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type |
"container" |
Yes | |
layout.top or layout.bottom |
number |
One of | Outer-box vertical placement. |
layout.left, layout.right, or layout.anchor |
One of | Outer-box horizontal placement. | |
width / height |
number |
Yes | Finite positive outer-box size. |
fill |
FillStyle |
No | Draws the container background only when declared. |
stroke |
StrokeStyle |
No | Draws the container outline only when declared. |
corner_radius |
number |
No | Single radius for all corners. |
layout.children.padding |
BoxSpacing |
No | top, right, bottom, left; child coordinates start in the content box. |
layout.children.overflow |
string | No | visible, clip, or paginate. Default visible. |
layout.children.mode |
string | No | coordinate or linear. Omitted mode means coordinate layout. |
layout.children.axis/gap/main_align/cross_align/wrap |
No | Linear-layout controls; valid only when mode is linear. |
|
defaults |
Defaults |
No | Applies only to descendants, not to the container’s own fill/stroke. |
elements |
array | Yes | Descendant elements. May be empty. |
Allowed descendants:
text,barcode,image,line,rect,circle,ellipse,polygon,path,link, andcontainer.tableis not a valid container child.
Layout:
- Coordinate layout keeps each child at its local
layout.left/layout.top,layout.right/layout.bottom, orlayout.anchor/layout.top. - Linear layout uses
layout.children.mode: "linear"plus optionallayout.children.axis,gap,main_align,cross_align, andwrap. - Linear children are measured from their own rendered bounds. A text child
without
style.width,frame.width, or an explicit box width is not automatically stretched to the container width; set child widths when wrapping, alignment, orcross_align: "stretch"must be predictable. axis:horizontal | vertical; defaultvertical.main_align:start | center | end | space_between; defaultstart.cross_align:start | center | end | stretch; defaultstart.wrap: boolean; defaultfalse.- With
axis = "horizontal",wrap: truestarts a new row when the next child would exceed the content-box width. - With
axis = "vertical",wrap: truestarts a new column when the next child would exceed the content-box height. - Each wrapped row or column uses the largest cross-axis size among the children
in that row/column. If content still exceeds the container, the container’s
layout.children.overflowbehaviour applies.
Overflow:
visibledraws children normally.clipclips descendants to the container outer box.paginatecreates continuation fragments across pages. It is allowed only in body-flow containers inpages[].elementsor nested inside another body-flow container. Descendant text splits only when that text item explicitly usesframe.overflow: "paginate"; otherwise children move whole by item boundary.
Link rule: if the container itself has link, descendants must not declare
element-level links and must not include standalone type: "link" items.
Paginated container with splittable text:
{
"type": "container",
"layout": {
"left": 10,
"top": 20,
"children": {
"mode": "coordinate",
"overflow": "paginate",
"padding": { "top": 2, "right": 2, "bottom": 2, "left": 2 }
}
},
"width": 80,
"height": 100,
"elements": [
{
"type": "text",
"layout": { "left": 0, "top": 0 },
"frame": { "width": 76, "overflow": "paginate" },
"content": {
"blocks": [
{
"type": "paragraph",
"inlines": [
{ "type": "text", "text": "Long text that may flow across pages." }
]
}
]
}
}
]
}
{
"type": "container",
"layout": {
"left": 20,
"top": 24,
"children": {
"mode": "coordinate",
"padding": { "top": 5, "right": 6, "bottom": 5, "left": 6 }
}
},
"width": 84,
"height": 34,
"fill": { "color": "#F8FAFC", "opacity": 1 },
"stroke": { "color": "#CBD5E1", "width": 0.3 },
"corner_radius": 3,
"elements": [
{
"type": "text",
"layout": { "left": 0, "top": 0 },
"style": { "width": 72, "font_size": 8, "color": "#64748B" },
"content": "Payment status"
},
{
"type": "text",
"layout": { "left": 0, "top": 10 },
"style": { "width": 72, "font_size": 15, "font_weight": "bold", "color": "#0F172A" },
"content": "Paid"
}
]
}
4.10 Table
A table renders tabular data with optional grouped headers, row headers, cell merging, alternate row fills, configurable grids, and page-break behaviour.
{
"type": "table",
"layout": { "left": 12, "top": 24 },
"width": 180,
"columns": [
{ "key": "sku", "header": "SKU", "width": { "mode": "fixed", "value": 30 } },
{ "key": "name", "header": "Name", "width": { "mode": "auto" } },
{ "key": "qty", "header": "Qty", "width": { "mode": "fixed", "value": 18 } },
{ "key": "amount", "header": "Amount", "width": { "mode": "fixed", "value": 30 },
"cell": { "text": { "text_align": "right" } } }
],
"rows": [
{ "sku": "A001", "name": "Widget", "qty": 2, "amount": "$120.00" },
{ "sku": "A002", "name": "Gadget", "qty": 1, "amount": "$60.00" }
],
"header": {
"show": true,
"repeat_on_page_break": true,
"cell": { "fill": { "color": "#F3F4F6" }, "text": { "font_weight": "bold" } }
},
"grid": {
"horizontal": { "color": "#D1D5DB", "width": 0.2 },
"vertical": false
}
}
Top-level fields:
| Field | Type | Required | Notes |
|---|---|---|---|
layout.left |
number |
Yes | Horizontal top-left placement (mm). |
layout.top |
number |
Yes | Vertical top-left placement (mm). |
width |
number |
No | Total table width. Required when any column uses percent, auto, or fit_content. |
columns |
TableColumn[] |
Yes | Column definitions. At least 1. |
rows |
TableRow[] |
Yes | Row data. May be empty. |
cell |
TableCellStyle |
No | Default cell style for the whole table. |
header |
TableHeaderConfig |
No | Column header configuration. |
row_header |
TableZoneConfig |
No | Row-header zone configuration. |
body |
TableBodyConfig |
No | Body-zone configuration. |
grid |
TableGridConfig |
No | Grid lines. |
pagination |
TablePaginationConfig |
No | Page-break behaviour. |
layout.z_index, comment |
No | Common element fields. |
4.10.1 Column
{
"key": "amount",
"header": "Amount",
"width": { "mode": "fixed", "value": 30 },
"role": "data",
"cell": { "text": { "text_align": "right" } },
"header_cell": { "text": { "font_weight": "bold" } }
}
| Field | Type | Required | Notes |
|---|---|---|---|
key |
string |
Yes | Unique within the table. |
header |
string |
No | Default empty string. |
width |
TableColumnWidth |
Yes | One of `fixed |
role |
string |
No | data (default) or row_header. |
cell |
TableCellStyle |
No | Per-column body cell style. |
header_cell |
TableCellStyle |
No | Per-column header cell style. |
TableColumnWidth:
[
{ "mode": "fixed", "value": 30 },
{ "mode": "percent", "value": 25 },
{ "mode": "auto" },
{ "mode": "fit_content" }
]
Rules:
columns[].keymust be unique.role = "row_header"columns must be contiguous on the left. You may have multiple row-header columns.fit_contentmeasures header and body content before flexible width allocation. Text, images, and barcodes participate in that measurement.autois also content-aware, but it expands to absorb remaining table width after fixed, percent, and fit-content allocation.- Column width and text wrapping are separate decisions. Use
wrap_policy = "no_wrap"in the relevantTextStylewhen text should stay on one line; do not invent a column width mode namedno_wrap.
4.10.2 Row and cell
rows is an array of objects keyed by columns[].key.
Shorthand cell (scalar value):
{ "name": "Apple", "qty": 2, "enabled": true, "note": null }
The shorthand form accepts only string, number, boolean, or null.
Rich block text, spans, images, barcodes, merges, per-cell style, and cell links
must use the complex-cell envelope under that row key.
Complex cell:
{
"group": {
"content": "Fruit",
"row_span": 2,
"col_span": 1,
"style": { "text": { "font_weight": "bold" } },
"link": { "target": { "type": "url", "url": "https://gpdf.com/docs/api-reference/#4102-row-and-cell" } }
}
}
Rich text cell:
{
"description": {
"content": {
"blocks": [
{
"type": "paragraph",
"content": [
{ "type": "text", "text": "Implementation services" }
]
}
]
}
}
}
Image and barcode cells use the same complex-cell envelope:
{
"logo": {
"image": {
"width": 24,
"height": 9,
"asset": "brand-logo",
"format": "png"
},
"style": { "text": { "text_align": "center" } }
},
"tracking_code": {
"barcode": {
"format": "code128",
"content": "GPDF-0001",
"width": 40,
"height": 12,
"barcode_text": {
"enabled": true,
"position": "bottom",
"offset": 1,
"style": { "font_size": 6, "text_align": "center" }
}
}
}
}
Complex cell fields:
| Field | Type | Notes |
|---|---|---|
content |
string | number | boolean | null | BlockTextContent |
Cell content. Scalars, or block text under the table profile (§4.6.4). |
image |
TableCellImage |
Cell image content. Body rows only. Mutually exclusive with content and barcode. |
barcode |
TableCellBarcode |
Cell barcode content. Body rows only. Mutually exclusive with content and image. |
row_span |
integer |
>= 1. Merge downward. |
col_span |
integer |
>= 1. Merge rightward. |
style |
TableCellStyle |
Per-cell override. |
link |
LinkSpec |
Cell-level link. Only on complex cells. |
Rules:
nullrenders as an empty string.booleanrenders as"true"/"false".- Rich block text is valid in body rows only as
cell.content. Do not put a bare{ "blocks": [...] }object directly as arows[]cell value. - A complex cell may set at most one of
content,image, orbarcode. imageuses the same source contract as §4.8, but omits element placementlayout,comment, andlink;widthandheightare required. Table-cell imagerotationmust be0.barcodeuses the sameformat,style,options, andbarcode_textcontract as §4.7, but omits element placementlayout,comment, andlink;format,content,width, andheightare required. Table-cell barcoderotationmust be0.- Image and barcode cells participate in row-height measurement and table pagination.
barcode_textheight and offset are included in the measured row height. style.text.text_aligncan align table-cell images and barcodes horizontally. Usecontent_offset_x/content_offset_yfor small manual adjustments.columns[].headerandheader.rows[].cells[]remain text/rich-text only; table-cell media is supported only in bodyrows.- Spans cannot exceed the table boundary or overlap each other. Violations return
API-002. - Rows containing a key not declared in
columns[].keyreturnAPI-002(no silent ignore).
4.10.3 Cell style
{
"padding": { "x": 1, "y": 1 },
"text": { "font_size": 9, "color": "#111111" },
"fill": { "color": "#FFFFFF" },
"content_offset_x": 1.5,
"content_offset_y": 0.5,
"borders": {
"top": false,
"right": { "color": "#111111", "width": 0.2 },
"bottom": { "color": "#111111", "width": 0.2 },
"left": false
}
}
Borders may be false (suppress) or a full StrokeStyle. Diagonal borders
are diagonal_tl_br and diagonal_bl_tr.
Do not put frame-like TextStyle fields such as width, height,
vertical_align, text_overflow, shrink_to_fit, or min_font_size inside
table.cell.text, header.cell.text, columns[].cell.text,
columns[].header_cell.text, or cell.style.text. Table cell text uses the
table text profile, which rejects frame-like fields. Use table width, column
width, padding, content_offset_x, content_offset_y, and text_align
instead.
4.10.4 Header / row header / body zones
Header:
{
"show": true,
"repeat_on_page_break": true,
"rows": [
{
"cells": [
{ "content": "Product", "col_span": 2 },
{ "content": "Stock", "row_span": 2 }
]
}
],
"cell": {
"fill": { "color": "#F3F4F6" },
"text": { "font_weight": "bold" }
}
}
Row header:
{
"cell": {
"fill": { "color": "#F8F8F8" },
"text": { "font_weight": "bold" }
}
}
Body:
{
"cell": {},
"alternate_fill": { "color": "#FAFAFA" }
}
Rules:
- Grouped column headers go in
header.rows. Leaf headers come fromcolumns[].header. header.rows[].cells[].contentandcolumns[].headeraccept the same value types as body cells.- The top-left corner cell is the leftmost row-header column’s own
headervalue. header.show = falseignoresheader.rows,columns[].header, andheader_cell.- Header text renders with middle vertical alignment by default. Do not set
text.vertical_aligninheader.cell,columns[].header_cell, grouped header cell styles, or body cell styles; table text usestable_text_profile, which rejects frame-like text style fields. Usepagination.header_min_height,padding.y, orcontent_offset_yfor visual adjustment.
4.10.5 Grid
{
"top": { "color": "#111111", "width": 0.3 },
"right": { "color": "#111111", "width": 0.3 },
"bottom": { "color": "#111111", "width": 0.3 },
"left": { "color": "#111111", "width": 0.3 },
"horizontal": { "color": "#D1D5DB", "width": 0.2 },
"vertical": false
}
Each side accepts false (suppress) or a full StrokeStyle. Double lines
use compound: { "kind": "double", "gap": <mm> } on the stroke.
4.10.6 Pagination
{
"keep_spans_together": true,
"row_min_height": 10,
"header_min_height": 12
}
| Field | Type | Default | Notes |
|---|---|---|---|
keep_spans_together |
boolean |
true |
Required true whenever any cell has row_span > 1. |
row_min_height |
number |
— | Minimum body row height (mm). |
header_min_height |
number |
— | Minimum header row height (mm). |
When a body table effectively has layout.flow: false, pagination.row_min_height
is required. If that table’s header is visible, pagination.header_min_height
is also required.
Body table pagination uses the measured page fragment height. Without margins,
the fragment limit is page.height - footer.layout.height and body offset is
0. With settings.layout.page_margin or pages[].layout.page_margin, body
offset is the effective top margin and the fragment limit is the smaller of
page.height - effective_bottom_margin and page.height - footer.layout.height.
The visible header consumes its measured header height on each fragment. Each
body row fits only when used_height + measured_row_height <= available_height;
measured row height includes text, padding, images, barcodes, barcode text, and
minimum heights. For strict documents, render-verify pagination instead of
assuming a fixed row count per page.
The public table contract supports minimum row heights, not fixed per-row
heights. There is no public row_height field and rows do not accept a
per-row height override. Approximate visual height with padding, font size,
content_offset_y, row_min_height, and header_min_height; use positioned
elements or a purpose-built template when strict fixed-row geometry is required.
4.10.7 Width and style precedence
Width:
columns.length >= 1.- All
columns[].widthusefixed,percent,auto, orfit_content. - If
table.widthis set:fixedcolumns reserve their mm.percentcolumns take a share oftable.width.fit_contentcolumns measure header/body content first, then take their preferred width when it fits; if preferred content width exceeds the remaining space, fit-content columns are scaled to fit.autocolumns absorb the remainder, sized by content measurement. When bothfit_contentandautoare present and their preferred widths exceed the remaining space, both flexible groups are scaled proportionally.- With no
autoorfit_contentcolumn, the resolved widths must filltable.widthexactly (otherwiseAPI-002).
- If
table.widthis omitted, all columns must befixed. - The sum of
percentwidths cannot exceed100.
Style precedence (later wins):
settings.defaultstable.cell- Zone-level
header.cell/row_header.cell/body.cell columns[].cell/columns[].header_cell- Per-cell
cell.style
Border precedence (later wins):
gridtable.cell.borders- Zone-level
cell.borders - Column-level
cell.borders/header_cell.borders - Per-cell
cell.style.borders
4.11 Table followed by container
Use sibling body elements for invoice and statement totals: a table followed
by one or more container elements. Enable flow on those siblings so the
container starts after the measured table height, including table pagination.
{
"type": "table",
"width": 84,
"columns": [
{ "key": "description", "header": "Description", "width": { "mode": "auto" } },
{ "key": "amount", "header": "Amount", "width": { "mode": "fixed", "value": 28 },
"cell": { "text": { "text_align": "right" } } }
],
"rows": [
{ "description": "Cloud compute", "amount": "$823.10" },
{ "description": "Replica storage", "amount": "$214.50" }
],
"layout": { "left": 8, "top": 24, "flow": true, "gap_after": 6 }
}
{
"type": "container",
"width": 84,
"height": 16,
"layout": { "left": 8, "top": 0, "flow": true },
"elements": [
{ "type": "text", "content": "Subtotal", "layout": { "left": 44, "top": 0 } },
{ "type": "text", "content": "$1,037.60", "style": { "width": 28, "text_align": "right" },
"layout": { "right": 0, "top": 0 } },
{ "type": "line", "x1": 44, "y1": 7, "x2": 84, "y2": 7 }
]
}
Rules:
- Put both elements directly in
pages[].elements. - Use
table.layout.flow: trueandtable.layout.gap_afterfor the vertical gap between the table and the trailing container. - Use
container.layout.flow: trueon the following container so it is placed after the table’s measured rendered height. - The trailing container uses normal container-local coordinates. Use
layout.rightfor right-aligned amounts inside the container. container.elementscannot containtable; put additional tables as sibling body elements.- Table pagination remains owned by the table. Following flow containers start only after the table has fully rendered.
4.12 Header and footer
header and footer are page-global decorations applied to every page.
{
"header": {
"layout": { "height": 14 },
"elements": [
{
"type": "text",
"content": "Monthly Report",
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 10,
"font_weight": "bold",
"color": "#111827",
"width": 80
},
"layout": {
"left": 12,
"top": 8
}
}
]
},
"footer": {
"layout": { "height": 12 },
"elements": [
{
"type": "text",
"content": "Page 1 / 12",
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8,
"color": "#6B7280",
"width": 38,
"text_align": "right"
},
"layout": {
"left": 54,
"top": 6
}
}
]
}
}
| Field | Type | Required | Notes |
|---|---|---|---|
layout.height |
number |
Yes | Region height (mm). |
elements |
Element[] |
No | Region elements. May be empty. |
header.elements and footer.elements may include table for repeated
tabular page furniture. Keep body line-item tables, totals, notes, and
signatures in pages[].elements so flow and table-followed-by-container layout
remain body-owned.
Coordinate semantics:
header.elementsuse section-local vertical coordinates. By convention place them insidelayout.top ∈ [0, header.layout.height].- For top-level positioned
header/footerelements with alayout.leftfield, horizontal placement is margin-relative whensettings.layout.page_marginorpages[].layout.page_marginis active: rendered left = effective left margin +layout.left. Negative sectionlayout.leftvalues may move content toward the paper edge, but they must not be less than-effective_left_margin; without margins, negative sectionlayout.leftvalues are invalid.layout.anchorkeeps normal anchor semantics. footer.elementsare auto-shifted bypage.height - footer.layout.heightat render time. Write them as iflayout.top = 0is the top of the footer region.- Header height does not push body elements down; body coordinates are controlled by
settings.layout.page_margin/pages[].layout.page_marginas described in §4.3. - Footer height is subtracted from the auto-paginated body region, so body overflow starts a new page above the footer.
Recommended:
- Set
header.layout.heightandfooter.layout.heightclose to actual content height. Oversized regions waste body space. - For header/footer divider rules, prefer a thin
rectwithwidth,height,fill, andlayout.left/layout.topinstead of a top-levelline.rectis a positioned element, so it follows the same margin-relative section placement as text. - For per-page-different headers/footers, do not use these global regions; embed the content in
pages[].elementsinstead.
Text inside header / footer follows the section profile (§4.6.4): no
list, no page_break, no frame.overflow = paginate, no multi-column.
4.13 Layers (background, watermark, stamp)
layers are document-level decorative layers. They do not participate in
body pagination and do not consume header / footer height. Use them
for diagonal “DRAFT” / “PRIVATE COPY” watermarks, page background colour
or paper textures, and “PAID” / approval stamps.
{
"layers": {
"background": {
"repeat": "all_pages",
"elements": [
{
"type": "rect",
"fill": { "color": "#FFFBEB" }
}
]
},
"watermark": {
"repeat": "all_pages",
"opacity": 0.12,
"template": {
"type": "text",
"content": "PRIVATE COPY"
},
"style": {
"font_family": "NotoSans-Regular",
"font_size": 10.5,
"font_weight": "bold",
"color": "#B91C1C",
"width": 56,
"text_align": "center"
},
"layout": {
"preset": "diagonal_tile",
"angle": 330,
"gap_x": 18, "gap_y": 16,
"offset_x": 8, "offset_y": 10,
"stagger_x": 34
}
},
"stamp": {
"repeat": "last_page",
"elements": [
{
"type": "circle",
"cx": 74, "cy": 120, "r": 12,
"stroke": { "color": "#B91C1C", "width": 0.9 }
},
{
"type": "text",
"layout": { "left": 62, "top": 116 },
"content": "PAID",
"style": {
"font_size": 10,
"font_weight": "bold",
"color": "#B91C1C",
"width": 24,
"text_align": "center"
}
}
]
}
}
}
Three layer slots:
| Slot | Render order | Spec shape | Use for |
|---|---|---|---|
background |
Behind body | repeat, elements[] |
Page colour, paper backgrounds, decorative frames. |
watermark |
Above body | Algorithmic spec: template + style + layout + opacity |
Diagonal-tiled “DRAFT” / “PRIVATE COPY” / brand text. |
stamp |
Top-most | repeat, elements[] |
“PAID” / approval marks, often on the last page. |
Common fields:
repeat:all_pages(default),first_page,last_page.
background / stamp element rules:
- Allowed types:
text,image,rect,line,circle,ellipse,polygon,path,link. - Forbidden types:
table. - For
rectinsidelayers.background.elements, omittedleft/topdefault to0, and omitted or0width/heightdefault to the current page size. Normalrectelements still require explicit geometry.
watermark rules:
template.typeis currently onlytext.layout.preset:center,tile,diagonal_tile,arc_outside,arc_inside,wave.- A single
watermarkselects onelayout.presetbehavior. Standard placement (center,tile,diagonal_tile) and path text placement (arc_outside,arc_inside,wave) are mutually exclusive. opacityis in[0, 1].- For
center,tile, anddiagonal_tile,layout.angleis text rotation in degrees. - For
arc_outsideandarc_inside, text is placed along a circular baseline. This is path text placement, not glyph-outline distortion.center_x/center_y: optional circle centre in mm. Defaults to page centre.radius: optional circle radius in mm. Defaults tomin(page.width, page.height) * 0.28.angle: optional arc anchor angle in degrees. Defaults to90forarc_outsideand270forarc_inside.
- For
wave, text is placed along a sine-wave baseline.start_x/start_y: optional wave start point in mm. Defaults to centred horizontally and page centre vertically.amplitude: optional wave amplitude in mm. Default6.wavelength: optional wave length in mm. Default42.
- Path watermark limits are enforced by validation:
- Single-line text only.
- At most 32 grapheme clusters.
style.width,style.height,style.text_overflow,style.shrink_to_fit,style.background,style.decoration,style.link_style, andstyle.wrap_policyare not supported for path watermarks.gap_x,gap_y, andstagger_xare only valid for tiled presets.
Path watermark example:
{
"template": {
"type": "text",
"content": "PRIVATE COPY"
},
"style": {
"font_size": 16,
"font_weight": "bold",
"color": "#B91C1C"
},
"opacity": 0.14,
"layout": {
"preset": "arc_outside",
"center_x": 105,
"center_y": 148,
"radius": 58,
"angle": 90
}
}
4.14 Settings
{
"settings": {
"defaults": {
"text": { "font_family": "NotoSans-Regular", "font_size": 11, "color": "#111111" },
"stroke": { "color": "#000000", "width": 0.4 },
"fill": { "color": "#FFFFFF", "opacity": 1.0 },
"shape": { "corner_radius": 0 }
},
"metadata": {
"title": "gPdf Document",
"author": "Acme Cloud Inc."
},
"output": {
"mode": "file",
"file_name": "archive-report-20260310.pdf"
},
"profile": "pdfa-2b",
"layout": {
"flow": true,
"gap_after": 4,
"pagination": {
"continuation_top_gap": 8,
"continuation_top_gap_with_header": 5
},
"page_margin": { "top": 10, "right": 12, "bottom": 10, "left": 12 }
}
}
}
| Field | Type | Notes |
|---|---|---|
defaults |
Defaults |
Global default styles. See §4.14.1. |
metadata |
Metadata |
PDF metadata. See §4.14.2. |
output |
OutputSettings |
Response shape. See §4.14.3. |
profile |
string |
PDF/A profile. See §6.4. |
layout |
DocumentLayout |
Global layout controls: flow, gap_after, start_top, page_margin, and pagination. |
e_invoice |
EInvoiceSettings |
Only valid on POST /api/v1/e-invoice/render. Sending it to JSON Render returns API-002. See §5. |
security |
SecuritySettings |
PDF password + permission protection. See §4.15. |
DocumentPaginationSettings:
| Field | Type | Default | Notes |
|---|---|---|---|
continuation_top_gap |
number |
8 |
Top gap in mm for generated continuation pages when no header is present and no settings.layout.page_margin / pages[].layout.page_margin is configured. |
continuation_top_gap_with_header |
number |
5 |
Gap in mm after header.layout.height for generated continuation pages when a header exists and no settings.layout.page_margin / pages[].layout.page_margin is configured. |
These fields only affect auto-paginated continuation content. They do not move first-page elements, which keep their absolute coordinates unless settings.layout.page_margin is configured. When settings.layout.page_margin or pages[].layout.page_margin is configured, continuation content starts at the next page’s content-box top instead of using these gaps.
settings.layout.flow:
- Default is disabled; omitted
flowpreserves explicitlayout.left/topbody layout. settings.layout.flow: trueaffects only bodypages[].elements, notheader,footer,layers, or watermark content.- Global flow does not skip placement validation. A normal body element that
participates through global
settings.layout.flow: truestill needs a finite, in-boxlayout.top; do not use largelayout.topvalues near the page bottom just to express source order. Use JSON order andlayout.gap_afterfor sequencing. Omitlayout.top/layout.bottomonly when that specific element declareslayout.flow: true. - Flow planning is per page. It does not flow elements from one explicit
pages[]entry into the next; plan each page’spages[].elementsarray independently. - Element-level
layout.flowoverrides the global setting:element.layout.flow ?? settings.layout.flow ?? false. - Gap-after precedence is
element.layout.gap_after ?? settings.layout.gap_after ?? 0. - A body element that uses
layout.bottomwithoutlayout.topis absolute-positioned and cannot participate inflow; setlayout.flow: falseon that element when globalsettings.layout.flowis enabled, or use top-based positioning instead. - Planning follows the JSON source array order, not
layout.z_index;layout.z_indexonly controls drawing order. - Each auto element’s
layout.topremains a design coordinate. The planner preserves the original vertical gap between neighbouring layout groups when it moves later elements. - With
settings.layout.page_marginorpages[].layout.page_margin, auto layout uses the same body content box coordinate system as ordinary body elements. - Tables advance following flow elements by their measured rendered height.
Text advances by measured height, unless explicit
frame.heightorstyle.heightcontrols the box height. - For table totals or notes, put a
containerafter thetableand enablelayout.flow: trueon both siblings. The table’slayout.gap_aftercontrols the gap before the trailing container. - Do not combine
layout.flow: truewith a container whoselayout.children.overflowispaginate; paginated containers already own their continuation flow.
4.14.1 Defaults
{
"defaults": {
"text": { "font_family": "NotoSans-Regular", "font_size": 11, "color": "#111111" },
"stroke": { "color": "#000000", "width": 0.4 },
"fill": { "color": "#FFFFFF", "opacity": 1.0 },
"shape": { "corner_radius": 0 }
}
}
| Subkey | Type | Notes |
|---|---|---|
text |
TextStyle |
Default text style. If font_family is set here without font_mode, strict is used. Use font_mode = "prefer" with the same font_family when this default should allow fallback, for example mixed Latin + CJK text. |
stroke |
StrokeStyle |
Default stroke for shapes and table grids. |
fill |
FillStyle |
Default fill. Default opacity is 0 (transparent) when omitted. |
shape |
ShapeDefaults |
corner_radius (mm) for default rounded rectangles. |
Do not set settings.defaults.fill just to make elements white. Prefer
element-specific fill on the rectangle, table header, cell, or container that
needs a visible background.
settings.defaults only accepts the four nested groups above. The legacy
flat fields font_family, font_size, color, unit are rejected with
API-002.
4.14.2 Metadata
{
"metadata": {
"title": "Monthly Report",
"author": "Acme Cloud Inc.",
"subject": "Operations digest",
"creator": "OpsBot 4.2",
"producer": "gPdf",
"language": "en"
}
}
| Field | Type | Notes |
|---|---|---|
title |
string |
|
author |
string |
|
subject |
string |
|
creator |
string |
The application that created the source content. |
producer |
string |
The renderer. |
language |
string |
BCP-47 language tag (e.g. en, de, zh-Hans). |
Normalisation:
- Empty or whitespace-only strings are treated as not provided.
- A token’s policy may strip metadata fields it has no permission for; the request still succeeds.
- Stripped fields fall back to the policy’s
default_metadata. title,creator,producer,languageuse system fallbacks when no value or default exists.authorandsubjectremain empty if no value or default exists.
4.14.3 Output
{
"output": {
"mode": "file",
"file_name": "invoice-20260310.pdf"
}
}
| Field | Type | Default | Notes |
|---|---|---|---|
mode |
"binary" | "file" |
binary |
Both return the same PDF bytes. Differs in Content-Disposition. |
file_name |
string |
Auto-generated gPdf-MMDDHHmmssSSS.pdf |
Sanitised; .pdf is appended automatically. |
Behaviour:
binary(or omitted):Content-Disposition: inline; filename="...". Browsers preview inline.file:Content-Disposition: attachment; filename="...". Browsers download.- Any value other than
binary/filereturnsAPI-002.
4.15 Security: PDF password and document permissions
settings.security turns on PDF standard-security-handler encryption
on the output PDF — AES-128 or AES-256, optional open password, optional
owner password, and eight per-action permission flags (print / copy /
modify / annotate / fill_forms / extract_accessibility / assemble /
print_high_quality). Only valid on POST /api/v1/pdf/render. Mutually
exclusive with settings.profile (PDF/A) and settings.e_invoice —
combining either returns API-002.
{
"settings": {
"security": {
"algorithm": "aes_128",
"open_password": "reader-demo",
"owner_password": "owner-demo",
"permissions": {
"print": true,
"modify": false,
"copy": false,
"annotate": false,
"fill_forms": true,
"extract_accessibility": true,
"assemble": false,
"print_high_quality": true
}
}
}
}
The example uses
aes_128to match the default and the lowest tier that ships encryption (Pro). For AES-256 — PDF 2.0 standard-security-handler revision 6 — switchalgorithmtoaes_256; that path is Enterprise-only (see tier policy below). Both algorithms accept the same field shape.
| Field | Type | Notes |
|---|---|---|
algorithm |
"aes_128" | "aes_256" |
Default aes_128. AES-128 = standard-security-handler revision 4 (R=4, V=4, AESV2 crypt filter, PDF 1.7 output). AES-256 = revision 6 (R=6, V=5, AESV3 crypt filter, PDF 2.0 output). |
open_password |
string |
Password required to open the PDF. Omitted, null, empty or whitespace-only = encryption disabled. Max 32 UTF-8 bytes. |
owner_password |
string |
Owner password granting full rights regardless of permissions. Must differ from open_password. Max 32 UTF-8 bytes. Enterprise policy. |
permissions |
Permissions |
8 booleans, default true. Restricting any flag requires owner_password. Enterprise policy. |
Permissions (each defaults to true):
| Flag | Effect when false |
|---|---|
print |
Printing is blocked. |
print_high_quality |
High-quality printing is blocked (a low-res render may still be allowed by print). |
modify |
Content edits other than annotations / form-field fill are blocked. |
copy |
Selecting and copying text / graphics is blocked. |
annotate |
Adding or modifying annotations AND form-field definitions is blocked. |
fill_forms |
Filling existing form fields is blocked (independent of annotate). |
extract_accessibility |
Extracting text / graphics for accessibility tools is blocked. |
assemble |
Inserting / rotating / deleting pages, and creating bookmarks / thumbnails, is blocked. |
Tier policy:
| Pro | Enterprise | |
|---|---|---|
| Algorithms | AES-128 only | AES-128 or AES-256 |
open_password |
✓ | ✓ |
owner_password |
— | ✓ |
permissions |
— | ✓ |
Behaviour:
- The metadata stream is encrypted alongside the rest of the document;
/EncryptMetadatawritestrue. - The token’s xAdmin PDF Policy must allow
document.security.allow = true, anddocument.security.allowed_algorithmsmust contain the requested algorithm. - A
settings.securityobject that contains no validopen_password, noowner_password, and no restrictedpermissionsis a no-op — the request renders an unencrypted PDF and does NOT enter the encryption write path. - Unrecognised
algorithmvalues are rejected at JSON deserialisation withAPI-001(settings.security.algorithm must be aes_128 or aes_256). - All other
settings.securitymisuses (password >32 UTF-8 bytes, policy disallow, combination withprofile/e_invoice, semantic violation) returnAPI-002with the offending field inmessage.
4.16 Default value precedence
When a field is omitted from an element, gPdf walks this chain to fill it in:
- The element’s own field (e.g.
line.stroke.width). settings.defaults(e.g.defaults.stroke.width).- Service-level defaults.
Font-family inheritance is not “fall back to auto when missing”. It is “keep walking up the chain until a font is declared, and only enter auto mode if the entire chain is silent”. Practical consequences:
- An element with
font_familyset continues to use that family even if children omit it. - Children inherit the explicit family until one explicitly overrides.
- Auto mode only activates when no ancestor has set a family at all.
5. E-Invoice Render API
The e-invoice family — Factur-X / ZUGFeRD packaging, capabilities, render, async strict-validation jobs, and artifact download — has its own dedicated reference at /docs/e-invoice-api/.
It moved out of this document on 2026-05-09 to give the e-invoice flows
(inline_pdf vs object, basic vs strict validation, residency
profiles) the room they need without bloating the JSON Render reference.
6. Reference
6.1 Error codes
All gPdf errors share the JSON envelope shown in §1.1.
Codes fall into four families: client (API-0xx), authentication (API-1xx),
billing/entitlement (API-2xx), and rendering/system (API-5xx / API-9xx).
| Code | HTTP | Family | Trigger | Typical message |
What to do |
|---|---|---|---|---|---|
API-001 |
400 |
Client | Body is not valid JSON. | Invalid JSON payload |
Validate with jq . or a JSON linter before sending. |
API-002 |
400 |
Client | Body parsed but failed schema or business validation. | <field> must be >= 0, Missing required field <name>, Field type mismatch |
Read message. The field name is always included. |
API-004 |
400 |
Client | Total page count exceeds the per-request limit (token policy or platform max, whichever is smaller). | page count exceeds max_pages_per_request |
Split the request into smaller batches, or request a higher policy limit. |
API-007 |
400 |
Client | Embedded image bytes exceed the renderer or active token policy limit. | image bytes exceeds max_image_bytes |
Re-encode the image at a smaller size, or use source.kind = "asset" to reference a pre-uploaded asset. |
API-008 |
413 |
Client | Request body exceeds the platform body limit (default 16 MiB; some deployments differ). |
Request body too large |
Reduce inline payload (especially base64 images). Consider splitting the document. |
API-101 |
401 |
Auth | Authorization header is missing or not in Bearer YOUR_TOKEN form. |
Missing or malformed Authorization header |
Add the header. The format is exactly Bearer followed by the token. |
API-102 |
403 |
Auth | Authentication failed (unknown token, invalid API surface, signature failure). | Authentication failed (redacted) |
Verify the token is active and valid for the public API. |
API-103 |
403 |
Auth | Token is blacklisted (revoked, suspended, or otherwise invalidated). | Authentication failed (redacted) |
Rotate the token via the Console, or contact support. |
API-201 |
402 |
Billing | No active subscription entitlement for this token. | No active subscription |
Activate or renew a plan in the Console. |
API-202 |
402 |
Billing | Subscription expired. | Subscription expired |
Renew in the Console. |
API-203 |
429 |
Billing | Subscription quota exceeded. | Quota exceeded |
Wait for the next billing cycle, top up, or upgrade the plan. |
API-204 |
402 |
Billing | Wallet balance insufficient for overage. | Insufficient wallet balance |
Top up via the Console. |
API-501 |
500 |
Render | PDF generation failed during rendering. | Detailed message describing the cause | Inspect message. Often points to invalid font, asset, or coordinate. |
API-502 |
500 |
Render | PDF/A compliance check failed after rendering. | PDF/A compliance check failed: <reason> |
Adjust the offending field (commonly fonts not in the embedded set, or non-PDF/A images). |
API-503 to API-507 |
500 |
Render | Specific rendering subsystem failures (font, asset resolution, layout). | Detailed message | Inspect message. These preserve actionable detail intentionally. |
API-504 |
500 |
Render | Resource loading failed. For fonts, this includes auto / prefer fallback exhaustion. |
Font fallback failed for: <run> or another resource message |
Provide a font_family that covers the script, upload the missing font as an asset, or verify the referenced image/font asset. |
API-900 |
500 |
System | Internal system error. | Redacted message | Retry once. If it persists, contact support with the req_id. |
API-999 |
500 |
System | Unknown internal error. | Redacted message | Same as API-900. |
Notes on validation errors (API-002):
API-002is the most common error. Common triggers include:- More than one of
left,right, andanchorprovided on the same element. - Custom
page.width/page.heightoutside the supported10..=2000 mmrange. font_modeprovided without a same-levelfont_family.- Explicit font in
strictmode that does not cover the submitted text. - Invalid
link(unsupported URL scheme, page index out of bounds, malformedpadding/border). - Invalid
table(unknown column key,table.widthcannot allocate a positive width to undeclared columns, invalid span). - Invalid
profilevalue. - Invalid
settings.security(password >32 UTF-8 bytes, policy doesn’t permit algorithm, combined withsettings.profileorsettings.e_invoice, orpermissionsprovided withoutowner_password). Unrecognisedalgorithmvalue is rejected at JSON deserialisation withAPI-001.
- More than one of
Notes on redaction:
API-102,API-103, andAPI-9xxdeliberately use generic messages. They will not tell you whether a token exists or why authentication failed. This is by design — to defeat token enumeration.API-201toAPI-204(billing) preserve actionable text so end users know whether to renew, top up, or wait for the cycle.API-501toAPI-507(render) preserve actionable text so engineering can debug.
6.2 Limits
Three kinds of limits apply to every request:
- Platform limits — fixed across all tenants, set per deployment.
- Policy limits — bound to the token, set when the plan is provisioned.
- Request-shape limits — encoded in the request schema itself.
| Limit | Default | Scope | Override path | Triggered error |
|---|---|---|---|---|
| Pages per request | 50 pages, or a lower token policy limit |
Platform plus token policy | Plan or per-token policy in the Console. | API-004 |
| Request body size | 16 MiB transport limit |
Platform | Per-deployment env var; some private deployments raise it. This is not a complexity or render-time guarantee. | API-008 |
| Image bytes per element | Raster images: 32 KiB; SVG: 256 KiB; deployments or token policy may be stricter |
Renderer plus token policy | Re-encode the image, use a smaller asset, or adjust the token policy if your plan allows it. | API-007 |
Template batch size (data array) |
10 items |
Platform | Not configurable. Split into multiple requests if you need more. | API-002 (Template render data item count ... exceeds max 10) |
| URL TTL for e-invoice artifacts | 900 seconds |
Request | delivery.url_ttl_seconds, range 1..900 |
API-002 if out of range |
| Retention for e-invoice artifacts | 23 hours |
Request | retention.ttl_hours, range 1..23 |
API-002 if out of range |
xml.content (e-invoice) |
2 MiB |
Platform | Not configurable. | API-002 |
How to pre-check on the client side:
- Check page count first. Split requests before they reach the active per-request page limit; page count is the most reliable public boundary for render work.
- Sum your request body bytes and reject above ~
14 MiBbefore sending. This leaves headroom for proxies and avoids the transport-levelAPI-008. - If you embed images via
source.kind = "base64", the base64 form is ~33% larger than the raw bytes. A 5 MiB raw JPEG becomes ~6.7 MiB in JSON. - If you batch templates, never put more than
10items indata. Use multiple HTTP calls for larger batches.
6.3 Response headers
| Header | Direction | Always present | Notes |
|---|---|---|---|
Content-Type |
Response | Yes | application/pdf on success, application/json on error. E-invoice job and artifact endpoints also return application/json. |
Content-Disposition |
Response | Yes (PDF responses only) | inline; filename="..." by default; attachment; filename="..." when output.mode = "file". |
X-Request-Id |
Both | Yes | Echoed from the request if the client supplied it; otherwise generated. |
Headers gPdf does not currently emit (and the absence is intentional in v1):
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-ResetRetry-AfterIdempotency-KeyechoX-Render-Time-MsX-Render-Engine-Version
Do not depend on these. If a future version adds any of them, this document will list them and clients can opt in.
6.4 Fonts and PDF/A profiles
Font resolution is controlled by font_family and font_mode:
- Auto: when no
font_familyis declared anywhere in the inheritance chain, the renderer chooses fonts that cover each text run from the bundled font set.font_mode = "auto"is not a public input value. prefer: whenfont_familyandfont_mode = "prefer"are declared in the same style object, the renderer tries the declared family first and falls back through bundled families for glyphs it cannot cover.strict: whenfont_familyis declared withfont_mode = "strict", or declared withoutfont_mode, the renderer must use that family. If the family cannot cover the text, the request fails withAPI-002.
Practical CJK guidance:
- If you do not declare
font_familyanywhere, auto mode can select bundled CJK fonts. - If you declare a Latin/default family such as
NotoSans-RegularorRobotoMono-Regularand the text may contain Chinese, Japanese, or Korean characters, setfont_mode = "prefer"in the same style object. - If you declare a family without
font_mode = "prefer", the request is strict. Mixed CJK text that the declared family cannot cover returnsAPI-002.
Failure modes:
- Strict coverage miss →
API-002. Adjust the text or pick a font that covers it, or switch the same style object tofont_mode = "prefer". - Auto / prefer total fallback miss →
API-504. The bundled set could not cover the text in any family.
Bundled CJK fallback maps Hangul to NotoSansKR-Regular, Japanese kana to
NotoSansJP-Regular, and CJK ideographs / fullwidth punctuation to
NotoSansSC-Regular. The bundled set also includes Latin, Greek, Cyrillic,
Arabic, Hebrew, Bengali, Tamil, Thai, Vietnamese, Osage, and monospace fonts.
This is not full Unicode coverage; unsupported scripts or symbols may still
fail in auto / prefer mode with API-504. Custom fonts can be uploaded as
assets via the Console and referenced by font_family.
Common built-in font family names that can be declared explicitly when needed:
RobotoMono-Regular, RobotoMono-Bold, NotoSans-Regular, NotoSans-Bold,
NotoSansSC-Regular, NotoSansSC-Bold, NotoSansJP-Regular,
NotoSansJP-Bold, NotoSansKR-Regular, NotoSansKR-Bold,
NotoSansArabic-Regular, NotoSansArabic-Bold, NotoSansThai-Regular,
NotoSansHebrew-Regular, NotoSansBengali-Regular,
NotoSansTamil-Regular, NotoSans_ru-RU, NotoSans_vi-VN,
NotoSansOsage-Regular, and Mansalva-Regular. When unsure, omit
font_family and let auto fallback choose.
PDF/A profiles:
| Profile | Use case |
|---|---|
pdfa-1b |
PDF/A-1b. Long-term archival, oldest baseline. |
pdfa-2b |
PDF/A-2b. Common archival profile. |
pdfa-3b |
PDF/A-3b. Required for embedded XML (e-invoice). |
pdfa-4 |
PDF/A-4. Newer archival profile. |
pdfa-2u |
PDF/A-2u. Unicode-mapped variant of 2b. |
pdfa-3u |
PDF/A-3u. Unicode-mapped variant of 3b. |
pdfa-ua1 |
PDF/UA-1. Accessibility profile. |
Set the profile via settings.profile. The chosen profile gates which
features can be used (e.g. transparency, embedded files). Violations return
API-502.
6.5 Coordinates and units
- Length unit: millimetre (mm). Floating-point values are accepted; the renderer rounds to PDF user-space at output.
- Origin: top-left of the page, or the content box if
page_marginis set. - Horizontal axis: rightward. Vertical axis: downward.
rotationis in degrees, clockwise.textandimageaccept arbitrary angles. Barcodes and their attachedbarcode_textshould use only0/90/180/270; the current PDF renderer treats other integer barcode angles as0.
Runnable coordinate map:
{
"settings": {
"layout": {
"page_margin": { "top": 8, "right": 8, "bottom": 8, "left": 8 }
}
},
"pages": [
{
"size": "label_100_150",
"elements": [
{
"type": "rect",
"layout": { "left": 0, "top": 0 },
"width": 84,
"height": 134,
"fill": { "color": "#F8FAFC" },
"stroke": { "color": "#CBD5E1", "width": 0.4 }
},
{
"type": "rect",
"layout": { "left": 0, "top": 0 },
"width": 2,
"height": 2,
"fill": { "color": "#2563EB" }
},
{
"type": "line",
"x1": 2,
"y1": 1,
"x2": 32,
"y2": 1,
"stroke": { "color": "#2563EB", "width": 0.5 }
},
{
"type": "line",
"x1": 1,
"y1": 2,
"x2": 1,
"y2": 32,
"stroke": { "color": "#2563EB", "width": 0.5 }
},
{
"type": "text",
"layout": { "left": 3, "top": 3 },
"content": "content origin (0,0)",
"style": { "font_size": 6.5, "color": "#1E3A8A", "width": 50 }
},
{
"type": "text",
"layout": { "left": 20, "top": 25 },
"content": "left=20, top=25",
"style": { "font_size": 7, "color": "#111827", "width": 35 }
},
{
"type": "rect",
"layout": { "left": 20, "top": 34 },
"width": 36,
"height": 20,
"fill": { "color": "#DBEAFE" },
"stroke": { "color": "#1D4ED8", "width": 0.6 }
},
{
"type": "text",
"layout": { "left": 21.5, "top": 41 },
"content": "36 x 20 mm",
"style": { "font_size": 7, "color": "#1E40AF", "width": 33, "text_align": "center" }
},
{
"type": "text",
"layout": { "left": 28, "top": 76 },
"rotation": 30,
"content": "30 degrees clockwise",
"style": { "font_size": 7, "color": "#B45309", "width": 38 }
}
]
}
]
}
In this example the physical page is 100 × 150 mm. The 8 mm page margin
makes the body content box 84 × 134 mm; every body element above uses
coordinates relative to that content-box origin.
7. Changelog
This document tracks the public API contract. Internal changes that do not affect callers are not listed here.
2026-05-31
- Docs-only canonical English rewrite (no API changes). The opening outline now follows the stronger localized structure: API surface, five-second start, common fallback rules, complete request skeleton, sandbox, response basics, and non-goals. §3 now explains the current
DocumentRequestsurfaces and the table / container layout decision model using behaviour verified against the current Rust schema and validators. - Docs-only English reference enrichment (no API changes). The overview now includes the full public route map and endpoint boundary notes. Quick Start now explains the intentionally omitted fields in the minimum request, the three public
text.contentforms, and common default fallback behaviour so the English source can serve as the base for future localized versions. - Docs-only coordinate example (no API changes). §6.5 now includes a runnable coordinate map that shows content-box origin, horizontal/vertical direction, millimetre dimensions, and clockwise rotation on a 100 × 150 mm label page.
2026-05-18
- Added constrained path-text watermark presets to
layers.watermark.layout.preset:arc_outside,arc_inside, andwave. The feature places short single-line text along circular or sine-wave baselines; it does not distort glyph outlines. Validation caps path watermark text at 32 grapheme clusters and rejects layout/style fields that would make rendering expensive or ambiguous.
2026-05-17
- Docs-only outline reshuffle (no API changes). §4.13 renamed
Layers→Layers (background, watermark, stamp)so the watermark feature is visible in the right-hand page outline. §4.14.4Security (password + permissions)promoted to its own H3 §4.15Security: PDF password and document permissionsfor the same reason. Old §4.15Default value precedencerenumbered to §4.16. Anchor URLs for §4.13, §4.15, and §4.16 changed; the §4.15 anchor that pointed at default-value-precedence now points at the security section, so update any external bookmarks accordingly. All field names (settings.security,layers.watermark, etc.) and behaviour are unchanged.
2026-05-16
- Added
settings.securityto JSON Render (POST /api/v1/pdf/render). Optional AES-128 or AES-256 PDF encryption withopen_password,owner_password(Enterprise policy), and 8 permission bits (print,modify,copy,annotate,fill_forms,extract_accessibility,assemble,print_high_quality). Passwords capped at 32 UTF-8 bytes. Mutually exclusive withsettings.profile(PDF/A) andsettings.e_invoice— both combinations returnAPI-002. Full field table, per-bit effect table, and tier matrix at §4.15 (was §4.14.4 at ship-time; renumbered 2026-05-17). - Added Security section to the request reference (originally §4.14.4, now §4.15).
- §6.1
API-002trigger list extended withsettings.securitymisuse cases (password >32 UTF-8 bytes, policy doesn’t permit algorithm, combined withsettings.profile/settings.e_invoice, orpermissionsprovided withoutowner_password). Unrecognisedalgorithmvalue returnsAPI-001at JSON deserialisation, notAPI-002.
2026-05-08
- First publication of the consolidated public API reference.
- Documented the e-invoice job and artifact endpoints (
GET /api/v1/e-invoice/jobs/{job_id}and.../artifacts/{artifact}). - Single consolidated error-code reference at §6.1.