Skip to content

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/test are distinguished solely by environment and token.


1. API Overview

RouteAuthenticationPurpose
POST /api/v1/renderAuthorization: Bearer <token>The only public request endpoint
  • Content-Type: application/json
  • Success response: application/pdf (binary stream)
  • Default output mode: binary
  • binary and file both return PDF bytes; the difference is only the Content-Disposition header

2. Request Structure

json
{
  "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

FieldTypeRequiredDescription
settingsSettingsNoGlobal settings
layersLayersNoGlobal repeated page layers
pagesPage[]YesPage array
headerSectionNoGlobal header
footerSectionNoGlobal footer

3.1.1 Page Sizing

Each page must explicitly define its size using one of two approaches:

  • size
  • width + height

Rules:

  • size and width/height are mutually exclusive
  • When size is not provided, both width and height must be specified
  • size matching 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:

  • background
  • watermark
  • stamp

Structure:

  • background
    • uses regular elements[]
  • watermark
    • uses an algorithmic spec: template + style + layout + opacity
  • stamp
    • uses regular elements[]

background / stamp shape:

json
{
  "repeat": "all_pages",
  "elements": []
}

watermark shape:

json
{
  "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:

  • repeat is optional; default is all_pages
  • Supported values:
    • all_pages
    • first_page
    • last_page
  • background renders before body content
  • watermark / stamp render after body content
  • stamp is the top-most layer, suitable for last-page stamps, approval marks, or final status badges
  • background.elements / stamp.elements only allow lightweight elements:
    • text
    • image
    • rect
    • line
    • circle
    • ellipse
    • polygon
    • link
  • table and stack are not allowed inside background or stamp
  • First version of watermark.template.type only supports:
    • text
  • watermark.layout.preset supports:
    • center
    • tile
    • diagonal_tile
  • watermark.opacity must be within 0..1
  • watermark.layout.angle is 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 rect or image
  • 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 with circle + line + text

Current boundary:

  • layers do not participate in body pagination and do not consume header/footer height
  • Regular element rotation still follows each element's documented rotation limits
  • watermark.layout.angle is specialized for watermark tiling
  • For semi-transparent image stamps, prefer PNG/SVG assets that already include alpha

Example: background + algorithmic watermark + last-page stamp

json
{
  "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 mm
  • a6 = 105 x 148 mm
  • letter = 215.9 x 279.4 mm
  • legal = 215.9 x 355.6 mm
  • label_100_100 = 100 x 100 mm
  • label_100_150 = 100 x 150 mm
  • label_4_6_in = 101.6 x 152.4 mm

Example:

json
{
  "pages": [
    { "size": "A4", "elements": [] },
    { "width": 100, "height": 150, "elements": [] }
  ]
}

3.1.2 Page Margin / Content Box

Optional configuration:

  • settings.page_margin
  • pages[].margin

Example:

json
{
  "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_margin is configured, pages[].elements x/y become relative to the content box top-left corner.
  • Body elements that exceed the content box trigger a validation error (no automatic clipping).
  • header/footer still use page coordinates and are not affected by page_margin.
  • When body content paginates to subsequent pages, it continues from the next page's content box top.

header and footer share the same structure:

json
{
  "height": 12,
  "elements": [
    { "type": "text", "x": 5, "y": 8, "content": "Page Header" }
  ]
}
FieldTypeRequiredDescription
heightnumberYesSection height in mm
elementsElement[]NoElements to render in this section

Semantics:

  • header is a global page header, applied to every page.
  • footer is a global page footer, applied to every page.
  • An empty elements array does not eliminate the section; do not rely on "empty array ignores the section."

Coordinates:

  • header.elements render in absolute page coordinates, typically within y = 0 .. header.height.
  • footer.elements are automatically offset to the page bottom by page.height - footer.height. Footer elements should use coordinates relative to the footer area, e.g. y = 0 .. footer.height.

Relationship with body:

  • header does 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.height affects the available body height calculation; body pagination avoids the footer area.
  • With page_margin configured, body pagination to subsequent pages starts from the next page's content box top.

Best practices:

  • Set header.height / footer.height close to the actual content height; avoid excessively large values.
  • For fixed decorative regions, use global header/footer instead of duplicating content on every page.
  • For per-page varying header/footer content, place them in pages[].elements instead.

3.3 Settings

FieldTypeRequiredDescription
defaultsDefaultsNoGlobal default styles
metadataMetadataNoPDF metadata
outputOutputSettingsNoResponse output mode and filename
profilestringNoPDF/A profile
page_marginPageMarginNoBody 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

FieldTypeRequiredDescription
modebinary | fileNoResponse presentation mode, default binary
file_namestringNoCustom filename, available in both binary and file modes

Rules:

  • binary or omitted: returns PDF bytes with Content-Disposition: inline; filename="...".
  • file: still returns PDF bytes, but uses Content-Disposition: attachment; filename="..." for download.
  • If file_name is omitted: falls back to gPdf-MMDDHHmmssSSS.pdf.
  • file_name is sanitized and automatically appended with .pdf.

3.4 Defaults

FieldTypeDescription
textTextStyleDefault text style
strokeStrokeStyleDefault stroke style
fillFillStyleDefault fill style
shapeShapeDefaultsDefault shape style

ShapeDefaults:

FieldTypeDescription
corner_radiusnumberDefault corner radius for rectangles (mm)

Note:

  • settings.defaults only accepts the structured format: text / stroke / fill / shape
  • settings.defaults.text is also a font scope; when it explicitly sets font_family and omits font_mode, the effective mode is strict

3.5 Metadata

FieldTypeDescription
titlestringTitle
authorstringAuthor
subjectstringSubject
creatorstringCreator tool
producerstringProducer
languagestringLanguage code

4. Element Types

Every item in elements[] must include a type field:

  • text
  • barcode
  • line
  • rect
  • circle
  • ellipse
  • polygon
  • link
  • image
  • table
  • stack

Common fields:

  • Most elements support z_index (default 0)
  • Most elements support comment (annotation only, not rendered)
  • Elements supporting rotation:
    • text supports arbitrary integer angles
    • image supports arbitrary angles
    • barcode and barcode-attached text currently only support 0/90/180/270
    • rect and ellipse should still use 0/90/180/270
  • Hyperlinks are supported in two modes:
    • Attaching a link field to elements (text/barcode/line/rect/circle/ellipse/polygon/image)
    • Independent hotspot element type: "link"

4.1 x_anchor (Horizontal Anchor Positioning)

x_anchor automatically calculates an element's final x based on a reference boundary.

Currently supported elements:

  • text
  • barcode
  • rect
  • image
  • link

Not supported:

  • line / circle / ellipse / polygon / table / stack / block

Rules:

  • x and x_anchor are mutually exclusive; providing both triggers an error
  • Without x_anchor, elements use the original x absolute positioning
  • When text uses x_anchor, width is required according to the chosen text form:
    • plain text / spans shorthand use style.width
    • block text uses frame.width
  • When barcode / rect / image / link use x_anchor, the element's own width is used
  • table_left / table_right are only allowed inside stack -> block

Reference values:

  • page_left
  • page_right
  • content_left
  • content_right
  • table_left
  • table_right

Calculation rules:

  • Left reference: resolved_x = reference + offset
  • Right reference: resolved_x = reference - offset - width

Example:

json
{
  "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

json
{
  "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 / square
  • join: miter / round / bevel
  • dash.preset: solid / dashed / dotted / custom
  • dash.pattern should only be provided when preset=custom
  • compound.kind: currently only double is supported
  • compound.gap: net gap between the two lines in mm

Notes:

  • Without compound, treated as a single line.
  • compound is shared by line, table.grid, and table.cell.borders.
  • double + dash is an invalid combination.
  • double + marker is an invalid combination.
  • rect/circle/ellipse/polygon will report an error if compound is provided.

5.2 FillStyle

json
{
  "color": "#F5F5F5",
  "opacity": 1.0,
  "rule": "nonzero"
}

rule: nonzero / even_odd

5.3 MarkerStyle (Line)

json
{
  "start": "none",
  "end": "arrow",
  "size": 2.5
}

start/end: none / arrow / open_arrow / circle / bar

json
{
  "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 color
  • width: line width (mm), 0 means no border drawn

Constraints:

  • URL supports only http://, https://, mailto:, tel:
  • URL whitespace is trimmed before writing
  • page starts from 1 and cannot exceed the number of explicit pages in the request
  • padding must be a finite number >= 0
  • border.width must be a finite number >= 0
  • border.color (if provided) must be a valid hex color
  • Any invalid link field triggers a ValidationError (never silently skipped)

6. Shape Elements

line/rect/circle/ellipse/polygon all support optional link: LinkSpec.

6.1 Line

json
{
  "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

json
{
  "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

json
{
  "type": "circle",
  "cx": 40,
  "cy": 40,
  "r": 12,
  "fill": { "color": "#E6F4FF" },
  "stroke": { "color": "#2B6CB0", "width": 0.5 }
}

6.4 Ellipse

json
{
  "type": "ellipse",
  "cx": 70,
  "cy": 40,
  "rx": 16,
  "ry": 10,
  "rotation": 0,
  "fill": { "color": "#FFF7E6" },
  "stroke": { "color": "#C05621", "width": 0.5 }
}

6.5 Polygon

json
{
  "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 }
}
json
{
  "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 spans shorthand: "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:

  • y
  • content
  • and exactly one of:
    • x
    • x_anchor

Plain text shorthand:

json
{
  "type": "text",
  "x": 18,
  "y": 18,
  "content": "Hello gPdf"
}

Rich spans shorthand:

json
{
  "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:

json
{
  "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:

FieldTypeRequiredNotes
type"text"yesElement type
ynumberyesPage coordinate in mm
xnumberone-ofPage coordinate in mm
x_anchorHorizontalAnchorone-ofAnchored positioning
rotationintegernoElement rotation in degrees
z_indexnumbernoLayer order
commentstringnoComment
_ifstringnoTemplate-pipeline metadata; POST /api/v1/render does not evaluate _if
linkLinkSpecnoElement-level link target
styleTextStylenoStyle for plain text / spans shorthand
frameBlockTextFramenoText frame style for block text
defaultsBlockTextDefaultsnoDefault styles for block text
contentstring | { spans: TextSpan[] } | BlockTextContentyesText content in any of the three supported forms

Top-level rules:

  • x and x_anchor are mutually exclusive
  • width is required when x_anchor is used:
    • plain text / spans shorthand use style.width
    • block text uses frame.width
  • text.link must not be combined with any inline link
  • rotation accepts integer degree values such as 0, 37, or -15

7.1.1 BlockTextDefaults

json
{
  "run": {},
  "paragraph": {},
  "frame": {}
}
  • run: inline default style
  • paragraph: paragraph default style
  • frame: frame default style

7.1.2 BlockTextContent

json
{
  "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:

  • paragraph
  • list
  • page_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

json
{
  "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 | justify
  • direction: auto | ltr | rtl
  • line_height
  • space_before
  • space_after
  • indent_left
  • indent_right
  • indent_first_line
  • hanging_indent
  • keep_together
  • keep_with_next
  • widow_orphan_control
  • tabs

7.1.4 List Block

json
{
  "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_profile
  • table_text_profile
  • barcode_text_profile

7.1.5 Inline Nodes

The public contract exposes four inline node types:

  • text
  • variable
  • line_break
  • tab

text example:

json
{
  "type": "text",
  "text": "Hello",
  "style": {
    "font_weight": "bold",
    "color": "#B91C1C"
  },
  "link": {
    "target": { "type": "url", "url": "https://example.com" }
  }
}

variable example:

json
{
  "type": "variable",
  "name": "page",
  "scope": "system"
}

variable.scope only allows:

  • system
  • binding
  • computed

Where:

  • system currently supports only page and total_pages for POST /api/v1/render
  • binding values come from the template data pipeline; use POST /api/v1/template-render for business-data substitution
  • computed is 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_family
  • font_size
  • font_weight: normal | medium | semibold | bold
  • font_style: normal | italic
  • font_mode: strict | prefer
  • color
  • opacity
  • letter_spacing
  • script: normal | superscript | subscript
  • background
  • decoration
  • link_style

Rules:

  • run-level style must not contain paragraph or frame fields
  • font_mode must not appear without a same-level font_family
  • font_mode = "auto" is not a public input value; automatic mode is derived only from the absence of explicit font intent in the current chain
  • strict coverage failures return API-002
  • automatic / prefer fallback exhaustion returns API-504

7.1.7 BlockTextFrame

frame supports:

  • width
  • height
  • vertical_align: top | middle | bottom
  • overflow: visible | clip | ellipsis | paginate
  • shrink_to_fit
  • min_font_size
  • padding
  • border
  • background
  • columns
  • column_gap

Rules:

  • frame is not allowed in table_text_profile or barcode_text_profile
  • rotation != 0 must not be combined with frame.overflow = "paginate"
  • frame.height must not be combined with page_break
  • frame.overflow = clip | ellipsis must not be combined with page_break
  • frame.overflow = paginate must not be combined with shrink_to_fit = true
  • section_text_profile does not allow overflow = paginate or 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
  • section_text_profile

    • header / 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_profile

    • barcode_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:

  • x
  • x_anchor

Optional: style, options, barcode_text, rotation, z_index, comment, link

Notes:

  • rotation currently supports only 0, 90, 180, and 270
  • barcode_text inherits barcode rotation, so it follows the same limit
  • format is case-insensitive, and - / _ are treated as equivalent separators
  • 2D / matrix formats encode as module grids; 1D / linear formats encode as bars; maxicode uses a hexagonal grid
  • The current build accepts these format values:
    • 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)

7.3 Image

Required fields: y, width, height, and exactly one of:

  • x
  • x_anchor

Optional: rotation, z_index, comment, link

Image source supports two mutually exclusive forms:

  • Shorthand: top-level asset, with optional top-level format
  • Explicit: top-level source

Rules:

  • asset and source are mutually exclusive
  • Top-level format is only valid with shorthand asset
  • When source is used, the format hint belongs inside source
  • Data URI (data:image/...;base64,...) is not supported

source.kind currently supports:

  • asset
  • base64

Notes:

  • asset is a service-managed asset key. It is not a client local filesystem path.
  • source.kind = "base64" accepts raw Base64 payload only, without the data:image/...;base64, prefix.
  • rotation is expressed in degrees and images accept arbitrary angles such as 45 or -30.

Shorthand asset:

json
{
  "type": "image",
  "x": 4,
  "y": 8,
  "width": 15,
  "height": 6.5,
  "asset": "pdn_express_logo",
  "format": "jpg"
}

Explicit asset source:

json
{
  "type": "image",
  "x": 4,
  "y": 8,
  "width": 15,
  "height": 6.5,
  "source": {
    "kind": "asset",
    "key": "pdn_express_logo",
    "format": "jpg"
  }
}

Explicit Base64 source:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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 table schema only supports the structure defined in this section.

Top-level structure:

json
{
  "type": "table",
  "x": 12,
  "y": 24,
  "width": 180,
  "columns": [],
  "rows": [],
  "cell": {},
  "header": {},
  "row_header": {},
  "body": {},
  "grid": {},
  "pagination": {}
}

Top-level fields:

FieldTypeRequiredDescription
xnumberYesTop-left X (mm)
ynumberYesTop-left Y (mm)
z_indexintegerNoZ-index
commentstringNoAnnotation
widthnumberNoTotal table width
columnsTableColumn[]YesColumn definitions
rowsTableRow[]YesRow data
cellTableCellStyleNoDefault cell style for the entire table
headerTableHeaderConfigNoColumn header configuration
row_headerTableZoneConfigNoRow header zone configuration
bodyTableBodyConfigNoBody zone configuration
gridTableGridConfigNoGrid line configuration
paginationTablePaginationConfigNoPagination configuration

7.4.1 Column

json
{
  "key": "amount",
  "header": "Amount",
  "width": { "mode": "fixed", "value": 30 },
  "role": "data",
  "cell": {
    "text": { "text_align": "right" }
  },
  "header_cell": {
    "text": { "font_weight": "bold" }
  }
}
FieldTypeRequiredDescription
keystringYesColumn key, must be unique
headerstringNoLeaf column header text, default empty
widthTableColumnWidthYesColumn width model
rolestringNodata / row_header, default data
cellTableCellStyleNoBody cell style for this column
header_cellTableCellStyleNoHeader cell style for this column

Rules:

  • columns[].key must be unique.
  • Columns with role = "row_header" must be contiguous and placed leftmost.
  • Multiple row header columns are supported.

TableColumnWidth:

json
{ "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:

json
{ "name": "Apple", "qty": 2, "enabled": true, "note": null }

Complex cell:

json
{
  "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:

FieldTypeRequiredDescription
contentstring | number | boolean | null | BlockTextContentNoCell content; accepts scalars or rich text
row_spanintegerNoNumber of rows to span downward
col_spanintegerNoNumber of columns to span rightward
styleTableCellStyleNoCell style override
linkLinkSpecNoCell hyperlink

Rules:

  • row_span >= 1
  • col_span >= 1
  • Spans cannot exceed boundaries or overlap with other spans
  • null renders as empty string
  • boolean renders as "true" / "false"
  • BlockTextContent follows table_text_profile
  • link can only appear in complex cell objects

7.4.3 TableCellStyle

json
{
  "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
  }
}
FieldTypeDescription
paddingTablePaddingCell padding
textTextStyleText style
fillFillStyleFill style
content_offset_xnumberHorizontal content offset (mm)
content_offset_ynumberVertical content offset (mm)
bordersTableBordersPer-side cell borders

TablePadding:

FieldTypeDescription
xnumberHorizontal padding (mm)
ynumberVertical padding (mm)

TableBorders:

  • top?: false | StrokeStyle
  • right?: false | StrokeStyle
  • bottom?: false | StrokeStyle
  • left?: false | StrokeStyle
  • diagonal_tl_br?: false | StrokeStyle
  • diagonal_bl_tr?: false | StrokeStyle

7.4.4 Header / Row Header / Body

header:

json
{
  "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, default true
  • repeat_on_page_break?: boolean, default true
  • rows?: TableHeaderRow[], grouped header rows; leaf headers still come from columns[].header
  • cell?: TableCellStyle

row_header:

json
{
  "cell": {
    "fill": { "color": "#F8F8F8" },
    "text": { "font_weight": "bold" }
  }
}

Fields:

  • cell?: TableCellStyle

body:

json
{
  "cell": {},
  "alternate_fill": { "color": "#FAFAFA" }
}

Fields:

  • cell?: TableCellStyle
  • alternate_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[].content and columns[].header both accept string | number | boolean | null | BlockTextContent

7.4.5 Grid

json
{
  "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 | StrokeStyle
  • right?: false | StrokeStyle
  • bottom?: false | StrokeStyle
  • left?: false | StrokeStyle
  • horizontal?: false | StrokeStyle
  • vertical?: false | StrokeStyle

Note:

  • grid.top/right/bottom/left/horizontal/vertical all use StrokeStyle

7.4.6 Pagination

json
{
  "keep_spans_together": true,
  "row_min_height": 10,
  "header_min_height": 12
}

Fields:

  • keep_spans_together?: boolean, default true
  • row_min_height?: number
  • header_min_height?: number

Note:

  • When row_span is used, keep_spans_together = true is required

7.4.7 Width & Style Rules

Width rules:

  • columns.length >= 1
  • columns[].width uses fixed / percent / auto
  • When table.width is provided:
    • fixed occupies space in mm first
    • percent allocates based on table.width percentage
    • auto takes remaining space, distributed based on header/body content measurement
    • Without auto, column widths must exactly fill table.width
  • Without table.width:
    • All columns must use fixed
  • percent sum must not exceed 100
  • Unknown keys in rows (not declared in columns[].key) trigger an error
  • When header.show = false, header.rows, columns[].header, and header_cell are ignored
  • When using row_span, keep_spans_together = false is not supported

Style priority:

  1. settings.defaults
  2. table.cell
  3. header.cell / row_header.cell / body.cell
  4. columns[].cell / columns[].header_cell
  5. cell.style

Border priority:

  1. grid
  2. table.cell.borders
  3. Zone-level cell.borders
  4. Column-level cell/header_cell.borders
  5. cell.style.borders

7.5 Stack / Block

Purpose:

  • stack is designed for invoice/statement scenarios where a table is followed by summary content.
  • It does not override table positioning; table.x/y/width retain their current semantics.
  • stack only manages the sequential layout and pagination between the table and subsequent content blocks.

Structure:

json
{
  "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:

FieldTypeRequiredDescription
gapnumberNoVertical gap between adjacent children in mm, default 0
childrenStackChild[]YesSequential layout children

Rules:

  • stack is only allowed in pages[].elements
  • stack.children[0] must be table
  • stack.children[1..] must be block
  • children.length >= 2

block:

json
{
  "type": "block",
  "elements": [
    { "type": "text", "x": 128, "y": 0, "content": "Subtotal" },
    { "type": "text", "x": 168, "y": 0, "content": "$1,343.65" }
  ]
}

Rules:

  • block does not define its own x/y/width/height
  • block.elements[].x uses existing body element semantics
  • block.elements[].y is relative to the start of the block
  • block cannot nest table / stack / block
  • Block height is automatically measured from its child elements

Pagination semantics:

  • The table paginates first using the existing logic
  • block layout only begins after the table has fully completed
  • If a block doesn't fit on the current page, it moves as a whole to the next page
  • If a single block is taller than one page's available body height, validation fails
  • gap is the vertical distance from the previous child's end position to the next child's start
  • When a block moves to the next page, the previous page's gap is 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

  1. Element's own fields (e.g. line.stroke.width)
  2. settings.defaults corresponding fields (e.g. defaults.stroke.width)
  3. System default configuration

table priority:

  1. settings.defaults
  2. table.cell
  3. header.cell / row_header.cell / body.cell
  4. columns[].cell / columns[].header_cell
  5. cell.style

9.2 Line/Shape Default Behavior

  • When line.stroke is entirely omitted:
    • color defaults to #000000
    • width defaults to 0.4
  • When rect/circle/ellipse/polygon.stroke is entirely omitted:
    • no border is drawn
    • if stroke is explicitly provided, missing fields continue to follow the public default chain
  • When fill is entirely omitted, no fill is applied (transparent)
  • rect.corner_radius default chain:
    • element.corner_radius
    • settings.defaults.shape.corner_radius
    • System default 0

10. Error Codes

CodeHTTPTrigger
API-001400Invalid JSON payload
API-002400Validation failure
API-003400Invalid or unsupported PDF/A profile
API-004400Page limit exceeded
API-005400Invalid page dimensions or margins
API-006400Invalid link parameters
API-007400Image payload size exceeded
API-008400Request body too large
API-009400Text form feed usage restriction
API-010400Stack layout parameter error
API-101401Missing or malformed Authorization header
API-102403Authentication failed
API-103403Authentication failed (token blacklisted)
API-501500PDF rendering failure
API-502500PDF/A compliance check failed
API-503500Layout overflow error
API-504500Resource loading failed
API-505500Font parsing failed
API-506500Image not found
API-507500Pagination internal error
API-900500Internal system error
API-901500Internal system error
API-902500Internal system error
API-999500Unknown 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, invalid padding/border)
  • Invalid table (unknown column key, table.width cannot allocate positive width for undeclared columns, invalid span)
  • Invalid profile
  • font_mode appears without a same-level font_family
  • An explicit strict font cannot cover the submitted text
  • x and x_anchor provided simultaneously

Boundary notes:

  • API-008 defaults to 16 MiB; different service deployments may configure a different limit
  • API-004 is not a single hard-coded number in all environments: the effective limit may be constrained by both service configuration and the active token policy
  • API-007 is policy-driven and applies only when the effective render policy enables an image-size limit