gPdf API Specification
Version: Unified API / Current Engine Updated: 2026-03-24
This document describes the current public schema. The only canonical route is
POST /api/v1/render.prod/testare distinguished solely by environment and token.
1. API Overview
| Route | Authentication | Purpose |
|---|---|---|
POST /api/v1/render | Authorization: Bearer <token> | The only public request endpoint |
Content-Type:application/json- Success response:
application/pdf(binary stream) - Default output mode:
binary binaryandfileboth return PDF bytes; the difference is only theContent-Dispositionheader
2. Request Structure
{
"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": "gPdf"
},
"output": {
"mode": "file",
"file_name": "invoice-20260310.pdf"
},
"profile": "pdfa-2b"
},
"layers": {
"background": {
"elements": []
},
"watermark": {
"elements": []
}
},
"header": {
"height": 12,
"elements": []
},
"footer": {
"height": 10,
"elements": []
},
"pages": [
{
"size": "label_100_150",
"elements": []
}
]
}3. Top-Level Objects
3.1 DocumentRequest
| Field | Type | Required | Description |
|---|---|---|---|
settings | Settings | No | Global settings |
layers | Layers | No | Global repeated page layers |
pages | Page[] | Yes | Page array |
header | Section | No | Global header |
footer | Section | No | Global footer |
3.1.1 Page Sizing
Each page must explicitly define its size using one of two approaches:
sizewidth+height
Rules:
sizeandwidth/heightare mutually exclusive- When
sizeis not provided, bothwidthandheightmust be specified sizematching is case-insensitive
3.1.2 Layers
layers defines document-level decorative layers that repeat independently of body pagination and do not consume header/footer layout height.
Supported slots:
backgroundwatermarkstamp
Structure:
background- uses regular
elements[]
- uses regular
watermark- uses an algorithmic spec:
template + style + layout + opacity
- uses an algorithmic spec:
stamp- uses regular
elements[]
- uses regular
background / stamp shape:
{
"repeat": "all_pages",
"elements": []
}watermark shape:
{
"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
}
}Rules:
repeatis optional; default isall_pages- Supported values:
all_pagesfirst_pagelast_page
backgroundrenders before body contentwatermark/stamprender after body contentstampis the top-most layer, suitable for last-page stamps, approval marks, or final status badgesbackground.elements/stamp.elementsonly allow lightweight elements:textimagerectlinecircleellipsepolygonlink
tableandstackare not allowed insidebackgroundorstamp- First version of
watermark.template.typeonly supports:text
watermark.layout.presetsupports:centertilediagonal_tile
watermark.opacitymust be within0..1watermark.layout.angleis expressed in degrees and supports arbitrary angles
Common patterns:
background- Best for full-page color fills, patterns, or background images
- The most common shape is one page-sized
rectorimage
watermark- Best for repeated hint text, light brand copy, and diagonal tiled notices
- You define one template and the service repeats it automatically using
layout
stamp- Best for last-page stamps, approval markers, or final-state badges
- A common pattern is
repeat: "last_page"combined withcircle + line + text
Current boundary:
layersdo not participate in body pagination and do not consumeheader/footerheight- Regular element rotation still follows each element's documented rotation limits
watermark.layout.angleis specialized for watermark tiling- For semi-transparent image stamps, prefer PNG/SVG assets that already include alpha
Example: background + algorithmic watermark + last-page stamp
{
"layers": {
"background": {
"elements": [
{
"type": "rect",
"x": 0,
"y": 0,
"width": 215.9,
"height": 279.4,
"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": 171,
"cy": 228,
"r": 18,
"stroke": { "color": "#B91C1C", "width": 0.9 }
},
{
"type": "text",
"x": 152,
"y": 220,
"content": "PAID",
"style": {
"font_size": 12,
"font_weight": "bold",
"color": "#B91C1C",
"width": 38,
"text_align": "center"
}
}
]
}
}
}Supported size presets:
a4=210 x 297 mma6=105 x 148 mmletter=215.9 x 279.4 mmlegal=215.9 x 355.6 mmlabel_100_100=100 x 100 mmlabel_100_150=100 x 150 mmlabel_4_6_in=101.6 x 152.4 mm
Example:
{
"pages": [
{ "size": "A4", "elements": [] },
{ "width": 100, "height": 150, "elements": [] }
]
}3.1.2 Page Margin / Content Box
Optional configuration:
settings.page_marginpages[].margin
Example:
{
"settings": {
"page_margin": { "top": 10, "right": 12, "bottom": 10, "left": 12 }
},
"pages": [
{
"size": "letter",
"margin": { "top": 8, "right": 10, "bottom": 12, "left": 10 },
"elements": []
}
]
}Rules:
- Without
page_margin, body elements use absolute page coordinates. - Once
page_marginis configured,pages[].elementsx/ybecome relative to thecontent boxtop-left corner. - Body elements that exceed the
content boxtrigger a validation error (no automatic clipping). header/footerstill use page coordinates and are not affected bypage_margin.- When body content paginates to subsequent pages, it continues from the next page's
content boxtop.
3.2 Section (header / footer)
header and footer share the same structure:
{
"height": 12,
"elements": [
{ "type": "text", "x": 5, "y": 8, "content": "Page Header" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
height | number | Yes | Section height in mm |
elements | Element[] | No | Elements to render in this section |
Semantics:
headeris a global page header, applied to every page.footeris a global page footer, applied to every page.- An empty
elementsarray does not eliminate the section; do not rely on "empty array ignores the section."
Coordinates:
header.elementsrender in absolute page coordinates, typically withiny = 0 .. header.height.footer.elementsare automatically offset to the page bottom bypage.height - footer.height. Footer elements should use coordinates relative to the footer area, e.g.y = 0 .. footer.height.
Relationship with body:
headerdoes not automatically push body elements down. Body elements still render in absolute page coordinates.- If using a header, position body elements to avoid the header area, e.g. starting from
y >= header.height. footer.heightaffects the available body height calculation; body pagination avoids the footer area.- With
page_marginconfigured, body pagination to subsequent pages starts from the next page'scontent boxtop.
Best practices:
- Set
header.height/footer.heightclose to the actual content height; avoid excessively large values. - For fixed decorative regions, use global
header/footerinstead of duplicating content on every page. - For per-page varying header/footer content, place them in
pages[].elementsinstead.
3.3 Settings
| Field | Type | Required | Description |
|---|---|---|---|
defaults | Defaults | No | Global default styles |
metadata | Metadata | No | PDF metadata |
output | OutputSettings | No | Response output mode and filename |
profile | string | No | PDF/A profile |
page_margin | PageMargin | No | Body content area page margins |
Supported profile values: pdfa-1b, pdfa-2b, pdfa-3b, pdfa-4, pdfa-2u, pdfa-3u, pdfa-ua1, etc.
3.3.1 OutputSettings
| Field | Type | Required | Description |
|---|---|---|---|
mode | binary | file | No | Response presentation mode, default binary |
file_name | string | No | Custom filename, available in both binary and file modes |
Rules:
binaryor omitted: returns PDF bytes withContent-Disposition: inline; filename="...".file: still returns PDF bytes, but usesContent-Disposition: attachment; filename="..."for download.- If
file_nameis omitted: falls back togPdf-MMDDHHmmssSSS.pdf. file_nameis sanitized and automatically appended with.pdf.
3.4 Defaults
| Field | Type | Description |
|---|---|---|
text | TextStyle | Default text style |
stroke | StrokeStyle | Default stroke style |
fill | FillStyle | Default fill style |
shape | ShapeDefaults | Default shape style |
ShapeDefaults:
| Field | Type | Description |
|---|---|---|
corner_radius | number | Default corner radius for rectangles (mm) |
Note:
settings.defaultsonly accepts the structured format:text / stroke / fill / shapesettings.defaults.textis also a font scope; when it explicitly setsfont_familyand omitsfont_mode, the effective mode isstrict
3.5 Metadata
| Field | Type | Description |
|---|---|---|
title | string | Title |
author | string | Author |
subject | string | Subject |
creator | string | Creator tool |
producer | string | Producer |
language | string | Language code |
4. Element Types
Every item in elements[] must include a type field:
textbarcodelinerectcircleellipsepolygonlinkimagetablestack
Common fields:
- Most elements support
z_index(default0) - Most elements support
comment(annotation only, not rendered) - Elements supporting rotation:
textsupports arbitrary integer anglesimagesupports arbitrary anglesbarcodeand barcode-attached text currently only support0/90/180/270rectandellipseshould still use0/90/180/270
- Hyperlinks are supported in two modes:
- Attaching a
linkfield to elements (text/barcode/line/rect/circle/ellipse/polygon/image) - Independent hotspot element
type: "link"
- Attaching a
4.1 x_anchor (Horizontal Anchor Positioning)
x_anchor automatically calculates an element's final x based on a reference boundary.
Currently supported elements:
textbarcoderectimagelink
Not supported:
line/circle/ellipse/polygon/table/stack/block
Rules:
xandx_anchorare mutually exclusive; providing both triggers an error- Without
x_anchor, elements use the originalxabsolute positioning - When
textusesx_anchor, width is required according to the chosen text form:- plain text /
spansshorthand usestyle.width - block text uses
frame.width
- plain text /
- When
barcode / rect / image / linkusex_anchor, the element's ownwidthis used table_left / table_rightare only allowed insidestack -> block
Reference values:
page_leftpage_rightcontent_leftcontent_righttable_lefttable_right
Calculation rules:
- Left reference:
resolved_x = reference + offset - Right reference:
resolved_x = reference - offset - width
Example:
{
"type": "text",
"x_anchor": { "reference": "content_right", "offset": 8 },
"y": 12,
"content": "$1,235.85",
"style": {
"width": 24,
"text_align": "right"
}
}5. Style Objects
5.1 StrokeStyle
{
"color": "#111111",
"width": 0.5,
"opacity": 1.0,
"cap": "butt",
"join": "miter",
"miter_limit": 10,
"dash": {
"preset": "dashed",
"pattern": [3, 2],
"phase": 0
},
"compound": {
"kind": "double",
"gap": 0.3
}
}Fields:
cap:butt/round/squarejoin:miter/round/beveldash.preset:solid/dashed/dotted/customdash.patternshould only be provided whenpreset=customcompound.kind: currently onlydoubleis supportedcompound.gap: net gap between the two lines in mm
Notes:
- Without
compound, treated as a single line. compoundis shared byline,table.grid, andtable.cell.borders.double + dashis an invalid combination.double + markeris an invalid combination.rect/circle/ellipse/polygonwill report an error ifcompoundis provided.
5.2 FillStyle
{
"color": "#F5F5F5",
"opacity": 1.0,
"rule": "nonzero"
}rule: nonzero / even_odd
5.3 MarkerStyle (Line)
{
"start": "none",
"end": "arrow",
"size": 2.5
}start/end: none / arrow / open_arrow / circle / bar
5.4 LinkSpec (Hyperlink)
{
"target": { "type": "url", "url": "https://example.com" },
"alt": "open official site",
"padding": 1.0,
"border": { "color": "#1A202C", "width": 0.3 }
}LinkTarget:
- URL:
{ "type": "url", "url": "https://..." } - Page destination:
{ "type": "page", "page": 2, "x": 10, "y": 20 }
LinkBorderStyle:
color: hex colorwidth: line width (mm),0means no border drawn
Constraints:
- URL supports only
http://,https://,mailto:,tel: - URL whitespace is trimmed before writing
pagestarts from1and cannot exceed the number of explicitpagesin the requestpaddingmust be a finite number>= 0border.widthmust be a finite number>= 0border.color(if provided) must be a valid hex color- Any invalid
linkfield triggers a ValidationError (never silently skipped)
6. Shape Elements
line/rect/circle/ellipse/polygon all support optional link: LinkSpec.
6.1 Line
{
"type": "line",
"x1": 4,
"y1": 99,
"x2": 96,
"y2": 99,
"stroke": {
"color": "#000000",
"width": 0.4,
"dash": { "preset": "solid" }
},
"link": {
"target": { "type": "url", "url": "https://example.com/spec" }
}
}When stroke is omitted, default values are applied (see Section 9).
6.2 Rect
{
"type": "rect",
"x_anchor": { "reference": "content_right", "offset": 6 },
"y": 20,
"width": 60,
"height": 20,
"fill": { "color": "#FFFFFF" },
"stroke": { "color": "#222222", "width": 0.6 },
"corner_radius": 2
}6.3 Circle
{
"type": "circle",
"cx": 40,
"cy": 40,
"r": 12,
"fill": { "color": "#E6F4FF" },
"stroke": { "color": "#2B6CB0", "width": 0.5 }
}6.4 Ellipse
{
"type": "ellipse",
"cx": 70,
"cy": 40,
"rx": 16,
"ry": 10,
"rotation": 0,
"fill": { "color": "#FFF7E6" },
"stroke": { "color": "#C05621", "width": 0.5 }
}6.5 Polygon
{
"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 }
}6.6 Link (Independent Hotspot)
{
"type": "link",
"x_anchor": { "reference": "content_left", "offset": 10 },
"y": 10,
"width": 40,
"height": 8,
"target": { "type": "url", "url": "https://example.com" },
"alt": "Open website"
}7. Other Elements
7.1 Text
Public text accepts three input forms:
- plain text shorthand:
"content": "Hello gPdf" - rich
spansshorthand:"content": { "spans": [...] } - block text:
"content": { "blocks": [...] }
All three forms are handled consistently during validation and rendering.
Block text is the most complete and recommended form for complex typography, variables, lists, and pagination control.
Required fields:
ycontent- and exactly one of:
xx_anchor
Plain text shorthand:
{
"type": "text",
"x": 18,
"y": 18,
"content": "Hello gPdf"
}Rich spans shorthand:
{
"type": "text",
"x": 18,
"y": 30,
"content": {
"spans": [
{ "text": "Hello ", "style": { "font_weight": "bold" } },
{ "text": "gPdf", "style": { "color": "#2563EB" } }
]
},
"style": {
"font_family": "NotoSans-Regular",
"font_size": 11,
"width": 80
}
}Block text:
{
"type": "text",
"x": 18,
"y": 42,
"rotation": 0,
"frame": {
"width": 120,
"overflow": "paginate"
},
"defaults": {
"run": {
"font_family": "NotoSans-Regular",
"font_size": 11,
"color": "#111111"
},
"paragraph": {
"align": "left",
"direction": "auto",
"line_height": 1.35
}
},
"content": {
"blocks": [
{
"type": "paragraph",
"inlines": [
{ "type": "text", "text": "Page " },
{ "type": "variable", "name": "page", "scope": "system" }
]
},
{ "type": "page_break" },
{
"type": "paragraph",
"inlines": [
{ "type": "text", "text": "Page " },
{ "type": "variable", "name": "page", "scope": "system" },
{ "type": "text", "text": " / " },
{ "type": "variable", "name": "total_pages", "scope": "system" }
]
}
]
}
}Top-level fields:
| Field | Type | Required | Notes |
|---|---|---|---|
type | "text" | yes | Element type |
y | number | yes | Page coordinate in mm |
x | number | one-of | Page coordinate in mm |
x_anchor | HorizontalAnchor | one-of | Anchored positioning |
rotation | integer | no | Element rotation in degrees |
z_index | number | no | Layer order |
comment | string | no | Comment |
_if | string | no | Template-pipeline metadata; POST /api/v1/render does not evaluate _if |
link | LinkSpec | no | Element-level link target |
style | TextStyle | no | Style for plain text / spans shorthand |
frame | BlockTextFrame | no | Text frame style for block text |
defaults | BlockTextDefaults | no | Default styles for block text |
content | string | { spans: TextSpan[] } | BlockTextContent | yes | Text content in any of the three supported forms |
Top-level rules:
xandx_anchorare mutually exclusive- width is required when
x_anchoris used:- plain text /
spansshorthand usestyle.width - block text uses
frame.width
- plain text /
text.linkmust not be combined with any inlinelinkrotationaccepts integer degree values such as0,37, or-15
7.1.1 BlockTextDefaults
{
"run": {},
"paragraph": {},
"frame": {}
}run: inline default styleparagraph: paragraph default styleframe: frame default style
7.1.2 BlockTextContent
{
"blocks": [
{
"type": "paragraph",
"inlines": [
{ "type": "text", "text": "Page " },
{ "type": "variable", "name": "page", "scope": "system" },
{ "type": "text", "text": " / " },
{ "type": "variable", "name": "total_pages", "scope": "system" }
]
}
]
}blocks currently exposes three block types:
paragraphlistpage_break
page_break is an explicit block node. The public block text contract no longer uses control syntax such as \f, {page}, {total_pages}, or inside plain strings.
7.1.3 Paragraph Block
{
"type": "paragraph",
"style": {
"align": "justify",
"direction": "auto",
"line_height": 1.35,
"space_after": 2.5,
"indent_first_line": 6
},
"inlines": [
{ "type": "text", "text": "Page " },
{ "type": "variable", "name": "page", "scope": "system" }
]
}paragraph.style supports:
align:left | center | right | justifydirection:auto | ltr | rtlline_heightspace_beforespace_afterindent_leftindent_rightindent_first_linehanging_indentkeep_togetherkeep_with_nextwidow_orphan_controltabs
7.1.4 List Block
{
"type": "list",
"list": {
"kind": "ordered",
"marker_gap": 2.5,
"item_spacing": 1.5,
"start_at": 1
},
"items": [
{
"blocks": [
{
"type": "paragraph",
"inlines": [{ "type": "text", "text": "Clause 1" }]
}
]
}
]
}list is only allowed in full_text_profile. It is not allowed in:
section_text_profiletable_text_profilebarcode_text_profile
7.1.5 Inline Nodes
The public contract exposes four inline node types:
textvariableline_breaktab
text example:
{
"type": "text",
"text": "Hello",
"style": {
"font_weight": "bold",
"color": "#B91C1C"
},
"link": {
"target": { "type": "url", "url": "https://example.com" }
}
}variable example:
{
"type": "variable",
"name": "page",
"scope": "system"
}variable.scope only allows:
systembindingcomputed
Where:
systemcurrently supports onlypageandtotal_pagesforPOST /api/v1/renderbindingvalues come from the template data pipeline; usePOST /api/v1/template-renderfor business-data substitutioncomputedis not currently exposed as a public capability
In other words, the direct render API currently guarantees only page-related system variables. Use the template API for business-data substitution or conditional filtering.
7.1.6 InlineTextStyle
Run-level style supports:
font_familyfont_sizefont_weight:normal | medium | semibold | boldfont_style:normal | italicfont_mode:strict | prefercoloropacityletter_spacingscript:normal | superscript | subscriptbackgrounddecorationlink_style
Rules:
- run-level style must not contain paragraph or frame fields
font_modemust not appear without a same-levelfont_familyfont_mode = "auto"is not a public input value; automatic mode is derived only from the absence of explicit font intent in the current chainstrictcoverage failures returnAPI-002- automatic /
preferfallback exhaustion returnsAPI-504
7.1.7 BlockTextFrame
frame supports:
widthheightvertical_align:top | middle | bottomoverflow:visible | clip | ellipsis | paginateshrink_to_fitmin_font_sizepaddingborderbackgroundcolumnscolumn_gap
Rules:
frameis not allowed intable_text_profileorbarcode_text_profilerotation != 0must not be combined withframe.overflow = "paginate"frame.heightmust not be combined withpage_breakframe.overflow = clip | ellipsismust not be combined withpage_breakframe.overflow = paginatemust not be combined withshrink_to_fit = truesection_text_profiledoes not allowoverflow = paginateor multi-column layout
7.1.8 Feature Profiles
Text capability is reduced by context. The profile is not sent in JSON; it is inferred from where the text appears:
full_text_profile- top-level body
text - blocks:
paragraph | list | page_break - inlines:
text | variable | line_break | tab
- top-level body
section_text_profileheader / footer / layers.background / layers.watermark / layers.stamp- blocks:
paragraph - inlines:
text | variable | line_break | tab - no
list - no
page_break - no
frame.overflow = paginate - no multi-column layout
table_text_profile- table text
- blocks:
paragraph - inlines:
text | variable | line_break - no
list - no
page_break - no
tab - no inline
link - no
frame
barcode_text_profilebarcode_text- blocks:
paragraph - inlines:
text | variable | line_break - no
list - no
page_break - no
tab - no inline
link - no
frame
7.2 Barcode
Required fields: y, format, content, width, height, and exactly one of:
xx_anchor
Optional: style, options, barcode_text, rotation, z_index, comment, link
Notes:
rotationcurrently supports only0,90,180, and270barcode_textinherits barcode rotation, so it follows the same limitformatis case-insensitive, and-/_are treated as equivalent separators- 2D / matrix formats encode as module grids; 1D / linear formats encode as bars;
maxicodeuses a hexagonal grid - The current build accepts these
formatvalues:- 2D / matrix:
qrcode(qr),microqr(micro-qr),pdf417,micropdf417(micro-pdf417),datamatrix(data-matrix),gs1datamatrix(gs1-datamatrix,gs1_datamatrix),aztec,maxicode,gs1qrcode(gs1-qrcode,gs1_qr,gs1-qr) - 1D / linear:
code128(code-128),code128a(code-128a),code128b(code-128b),code128c(code-128c),gs1128(gs1-128),code39(code-39),code93(code-93),codabar,ean8(ean-8),ean13(ean-13),upca(upc-a),upce(upc-e),itf(interleaved2of5),itf14(itf-14),gtin8(gtin-8),gtin12(gtin-12),gtin13(gtin-13),gtin14(gtin-14),isbn(isbn-13),sscc(sscc-18) - Other business formats:
msi(msi-plessey),msi10(msi-10),msi11(msi-11),msi1010(msi-1010),msi1110(msi-1110),upus10(s10),uspsimb(usps-imb,intelligent-mail),upcacomposite(upca-composite),upcecomposite(upce-composite)
- 2D / matrix:
7.3 Image
Required fields: y, width, height, and exactly one of:
xx_anchor
Optional: rotation, z_index, comment, link
Image source supports two mutually exclusive forms:
- Shorthand: top-level
asset, with optional top-levelformat - Explicit: top-level
source
Rules:
assetandsourceare mutually exclusive- Top-level
formatis only valid with shorthandasset - When
sourceis used, the format hint belongs insidesource - Data URI (
data:image/...;base64,...) is not supported
source.kind currently supports:
assetbase64
Notes:
assetis a service-managed asset key. It is not a client local filesystem path.source.kind = "base64"accepts raw Base64 payload only, without thedata:image/...;base64,prefix.rotationis expressed in degrees and images accept arbitrary angles such as45or-30.
Shorthand asset:
{
"type": "image",
"x": 4,
"y": 8,
"width": 15,
"height": 6.5,
"asset": "pdn_express_logo",
"format": "jpg"
}Explicit asset source:
{
"type": "image",
"x": 4,
"y": 8,
"width": 15,
"height": 6.5,
"source": {
"kind": "asset",
"key": "pdn_express_logo",
"format": "jpg"
}
}Explicit Base64 source:
{
"type": "image",
"x": 4,
"y": 8,
"width": 15,
"height": 6.5,
"source": {
"kind": "base64",
"format": "jpg",
"payload": "/9j/4AAQSkZJRgABAQ..."
}
}7.3.1 Core styled JSON examples
Styled text:
{
"type": "text",
"x": 18,
"y": 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
}
}Styled barcode:
{
"type": "barcode",
"x": 18,
"y": 34,
"format": "qrcode",
"content": "https://example.com/track/INV-2026-001",
"width": 28,
"height": 28,
"style": {
"color": "#111111",
"background_color": "#FFFFFF"
},
"barcode_text": {
"enabled": true,
"position": "bottom",
"offset": 1.5,
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8,
"color": "#374151",
"width": 28,
"text_align": "center"
}
}
}Styled rect:
{
"type": "rect",
"x": 12,
"y": 70,
"width": 80,
"height": 18,
"corner_radius": 2,
"fill": {
"color": "#F9FAFB",
"opacity": 1
},
"stroke": {
"color": "#D1D5DB",
"width": 0.4
}
}Styled image:
{
"type": "image",
"x": 14,
"y": 92,
"width": 18,
"height": 9,
"rotation": -8,
"asset": "pdn_express_logo",
"format": "jpg",
"link": {
"target": {
"type": "url",
"url": "https://example.com"
}
}
}Styled header / footer:
{
"header": {
"height": 14,
"elements": [
{
"type": "text",
"x": 12,
"y": 8,
"content": "Monthly Report",
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 10,
"font_weight": "bold",
"color": "#111827",
"width": 80
}
}
]
},
"footer": {
"height": 12,
"elements": [
{
"type": "text",
"x": 150,
"y": 6,
"content": "Page 1 / 12",
"style": {
"font_family": "NotoSans-Regular",
"font_mode": "prefer",
"font_size": 8,
"color": "#6B7280",
"width": 40,
"text_align": "right"
}
}
]
}
}7.4 Table
Note:
- The current public
tableschema only supports the structure defined in this section.
Top-level structure:
{
"type": "table",
"x": 12,
"y": 24,
"width": 180,
"columns": [],
"rows": [],
"cell": {},
"header": {},
"row_header": {},
"body": {},
"grid": {},
"pagination": {}
}Top-level fields:
| Field | Type | Required | Description |
|---|---|---|---|
x | number | Yes | Top-left X (mm) |
y | number | Yes | Top-left Y (mm) |
z_index | integer | No | Z-index |
comment | string | No | Annotation |
width | number | No | Total table width |
columns | TableColumn[] | Yes | Column definitions |
rows | TableRow[] | Yes | Row data |
cell | TableCellStyle | No | Default cell style for the entire 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 line configuration |
pagination | TablePaginationConfig | No | Pagination configuration |
7.4.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 | Description |
|---|---|---|---|
key | string | Yes | Column key, must be unique |
header | string | No | Leaf column header text, default empty |
width | TableColumnWidth | Yes | Column width model |
role | string | No | data / row_header, default data |
cell | TableCellStyle | No | Body cell style for this column |
header_cell | TableCellStyle | No | Header cell style for this column |
Rules:
columns[].keymust be unique.- Columns with
role = "row_header"must be contiguous and placed leftmost. - Multiple row header columns are supported.
TableColumnWidth:
{ "mode": "fixed", "value": 30 }
{ "mode": "percent", "value": 25 }
{ "mode": "auto" }7.4.2 Row / Cell
rows is an array of objects, with keys corresponding to columns[].key.
Simple cell:
{ "name": "Apple", "qty": 2, "enabled": true, "note": null }Complex cell:
{
"group": {
"content": "Fruit",
"row_span": 2,
"col_span": 1,
"style": {
"text": { "font_weight": "bold" }
},
"link": {
"target": { "type": "url", "url": "https://example.com" }
}
}
}Complex cell fields:
| Field | Type | Required | Description |
|---|---|---|---|
content | string | number | boolean | null | BlockTextContent | No | Cell content; accepts scalars or rich text |
row_span | integer | No | Number of rows to span downward |
col_span | integer | No | Number of columns to span rightward |
style | TableCellStyle | No | Cell style override |
link | LinkSpec | No | Cell hyperlink |
Rules:
row_span >= 1col_span >= 1- Spans cannot exceed boundaries or overlap with other spans
nullrenders as empty stringbooleanrenders as"true"/"false"BlockTextContentfollowstable_text_profilelinkcan only appear in complex cell objects
7.4.3 TableCellStyle
{
"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
}
}| Field | Type | Description |
|---|---|---|
padding | TablePadding | Cell padding |
text | TextStyle | Text style |
fill | FillStyle | Fill style |
content_offset_x | number | Horizontal content offset (mm) |
content_offset_y | number | Vertical content offset (mm) |
borders | TableBorders | Per-side cell borders |
TablePadding:
| Field | Type | Description |
|---|---|---|
x | number | Horizontal padding (mm) |
y | number | Vertical padding (mm) |
TableBorders:
top?: false | StrokeStyleright?: false | StrokeStylebottom?: false | StrokeStyleleft?: false | StrokeStylediagonal_tl_br?: false | StrokeStylediagonal_bl_tr?: false | StrokeStyle
7.4.4 Header / Row Header / Body
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" }
}
}Fields:
show?: boolean, defaulttruerepeat_on_page_break?: boolean, defaulttruerows?: TableHeaderRow[], grouped header rows; leaf headers still come fromcolumns[].headercell?: TableCellStyle
row_header:
{
"cell": {
"fill": { "color": "#F8F8F8" },
"text": { "font_weight": "bold" }
}
}Fields:
cell?: TableCellStyle
body:
{
"cell": {},
"alternate_fill": { "color": "#FAFAFA" }
}Fields:
cell?: TableCellStylealternate_fill?: FillStyle
Expression rules:
- Grouped headers use
header.rows - Leaf column headers use
columns[].header - Row headers use
columns[].role = "row_header" header.rows[].cells[].contentandcolumns[].headerboth acceptstring | number | boolean | null | BlockTextContent
7.4.5 Grid
{
"top": {
"color": "#111111",
"width": 0.3,
"compound": { "kind": "double", "gap": 0.3 }
},
"right": {
"color": "#111111",
"width": 0.3,
"compound": { "kind": "double", "gap": 0.3 }
},
"bottom": {
"color": "#111111",
"width": 0.3,
"compound": { "kind": "double", "gap": 0.3 }
},
"left": {
"color": "#111111",
"width": 0.3,
"compound": { "kind": "double", "gap": 0.3 }
},
"horizontal": { "color": "#D1D5DB", "width": 0.2 },
"vertical": false
}Fields:
top?: false | StrokeStyleright?: false | StrokeStylebottom?: false | StrokeStyleleft?: false | StrokeStylehorizontal?: false | StrokeStylevertical?: false | StrokeStyle
Note:
grid.top/right/bottom/left/horizontal/verticalall useStrokeStyle
7.4.6 Pagination
{
"keep_spans_together": true,
"row_min_height": 10,
"header_min_height": 12
}Fields:
keep_spans_together?: boolean, defaulttruerow_min_height?: numberheader_min_height?: number
Note:
- When
row_spanis used,keep_spans_together = trueis required
7.4.7 Width & Style Rules
Width rules:
columns.length >= 1columns[].widthusesfixed / percent / auto- When
table.widthis provided:fixedoccupies space in mm firstpercentallocates based ontable.widthpercentageautotakes remaining space, distributed based on header/body content measurement- Without
auto, column widths must exactly filltable.width
- Without
table.width:- All columns must use
fixed
- All columns must use
percentsum must not exceed100- Unknown keys in
rows(not declared incolumns[].key) trigger an error - When
header.show = false,header.rows,columns[].header, andheader_cellare ignored - When using
row_span,keep_spans_together = falseis not supported
Style priority:
settings.defaultstable.cellheader.cell / row_header.cell / body.cellcolumns[].cell / columns[].header_cellcell.style
Border priority:
gridtable.cell.borders- Zone-level
cell.borders - Column-level
cell/header_cell.borders cell.style.borders
7.5 Stack / Block
Purpose:
stackis designed for invoice/statement scenarios where atableis followed by summary content.- It does not override table positioning;
table.x/y/widthretain their current semantics. stackonly manages the sequential layout and pagination between thetableand subsequent content blocks.
Structure:
{
"type": "stack",
"gap": 6,
"children": [
{
"type": "table",
"x": 18,
"y": 123,
"width": 180,
"columns": [],
"rows": []
},
{
"type": "block",
"elements": [
{ "type": "text", "x": 128, "y": 0, "content": "Subtotal" },
{ "type": "text", "x": 168, "y": 0, "content": "$1,343.65" },
{ "type": "line", "x1": 128, "y1": 7, "x2": 178, "y2": 7 }
]
}
]
}Top-level fields:
| Field | Type | Required | Description |
|---|---|---|---|
gap | number | No | Vertical gap between adjacent children in mm, default 0 |
children | StackChild[] | Yes | Sequential layout children |
Rules:
stackis only allowed inpages[].elementsstack.children[0]must betablestack.children[1..]must beblockchildren.length >= 2
block:
{
"type": "block",
"elements": [
{ "type": "text", "x": 128, "y": 0, "content": "Subtotal" },
{ "type": "text", "x": 168, "y": 0, "content": "$1,343.65" }
]
}Rules:
blockdoes not define its ownx/y/width/heightblock.elements[].xuses existing body element semanticsblock.elements[].yis relative to the start of the blockblockcannot nesttable/stack/block- Block height is automatically measured from its child elements
Pagination semantics:
- The
tablepaginates first using the existing logic blocklayout only begins after thetablehas fully completed- If a
blockdoesn't fit on the current page, it moves as a whole to the next page - If a single
blockis taller than one page's available body height, validation fails gapis the vertical distance from the previous child's end position to the next child's start- When a
blockmoves to the next page, the previous page'sgapis not preserved
8. Coordinates & Units
- Coordinate unit: mm
- Origin: top-left
(0, 0) - X-axis points right, Y-axis points down
9. Default Value Priority
9.1 Style Priority
- Element's own fields (e.g.
line.stroke.width) settings.defaultscorresponding fields (e.g.defaults.stroke.width)- System default configuration
table priority:
settings.defaultstable.cellheader.cell / row_header.cell / body.cellcolumns[].cell / columns[].header_cellcell.style
9.2 Line/Shape Default Behavior
- When
line.strokeis entirely omitted:colordefaults to#000000widthdefaults to0.4
- When
rect/circle/ellipse/polygon.strokeis entirely omitted:- no border is drawn
- if
strokeis explicitly provided, missing fields continue to follow the public default chain
- When
fillis entirely omitted, no fill is applied (transparent) rect.corner_radiusdefault chain:element.corner_radiussettings.defaults.shape.corner_radius- System default
0
10. Error Codes
| Code | HTTP | Trigger |
|---|---|---|
| API-001 | 400 | Invalid JSON payload |
| API-002 | 400 | Validation failure |
| API-003 | 400 | Invalid or unsupported PDF/A profile |
| API-004 | 400 | Page limit exceeded |
| API-005 | 400 | Invalid page dimensions or margins |
| API-006 | 400 | Invalid link parameters |
| API-007 | 400 | Image payload size exceeded |
| API-008 | 400 | Request body too large |
| API-009 | 400 | Text form feed usage restriction |
| API-010 | 400 | Stack layout parameter error |
| API-101 | 401 | Missing or malformed Authorization header |
| API-102 | 403 | Authentication failed |
| API-103 | 403 | Authentication failed (token blacklisted) |
| API-501 | 500 | PDF rendering failure |
| API-502 | 500 | PDF/A compliance check failed |
| API-503 | 500 | Layout overflow error |
| API-504 | 500 | Resource loading failed |
| API-505 | 500 | Font parsing failed |
| API-506 | 500 | Image not found |
| API-507 | 500 | Pagination internal error |
| API-900 | 500 | Internal system error |
| API-901 | 500 | Internal system error |
| API-902 | 500 | Internal system error |
| API-999 | 500 | Unknown internal error |
Redaction behavior:
API-102,API-103, and system errors (API-900/API-901/API-902/API-999) return redacted public messages.- Client input errors and render errors (
API-501~API-507) preserve actionable details.
Common ValidationError triggers:
- Invalid
link(unsupported URL scheme, page index out of bounds, invalidpadding/border) - Invalid
table(unknown column key,table.widthcannot allocate positive width for undeclared columns, invalid span) - Invalid
profile font_modeappears without a same-levelfont_family- An explicit
strictfont cannot cover the submitted text xandx_anchorprovided simultaneously
Boundary notes:
API-008defaults to16 MiB; different service deployments may configure a different limitAPI-004is not a single hard-coded number in all environments: the effective limit may be constrained by both service configuration and the active token policyAPI-007is policy-driven and applies only when the effective render policy enables an image-size limit