Skip to content

xPdf API Specification

Version: Unified API / V2 Engine Updated: 2026-03-10

This document describes the current public schema. The only canonical route is POST /api/v1/label. prod/test are distinguished solely by environment and token.


1. API Overview

RouteAuthenticationPurpose
POST /api/v1/labelAuthorization: Bearer <token>The only public request endpoint
  • Content-Type: application/json
  • Response: application/pdf (binary stream)
  • Default output mode: binary
  • When settings.output.mode = "file", the response includes Content-Disposition

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": "xPdf Document",
      "author": "xPdf"
    },
    "output": {
      "mode": "file",
      "file_name": "invoice-20260310.pdf"
    },
    "profile": "pdfa-2b"
  },
  "header": {
    "height": 12,
    "elements": []
  },
  "footer": {
    "height": 10,
    "elements": []
  },
  "pages": [
    {
      "size": "label_100_150",
      "elements": []
    }
  ]
}

3. Top-Level Objects

3.1 DocumentRequest

FieldTypeRequiredDescription
settingsSettingsNoGlobal settings
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

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 | fileNoOutput mode, default binary
file_namestringNoCustom filename, only effective when mode = "file"

Rules:

  • binary or omitted: returns only the PDF binary.
  • file: returns with Content-Disposition for file 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

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, barcode, rect, ellipse, image (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, style.width is required
  • 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

Required fields: y, content, and exactly one of:

  • x
  • x_anchor

style uses TextStyle:

  • font_family, font_size, font_weight, color
  • text_align, vertical_align, line_height
  • width, height, text_overflow
  • shrink_to_fit, min_font_size
  • Optional link: LinkSpec

Supports variables: {page}, {total_pages}.

7.2 Barcode

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

  • x
  • x_anchor

Optional: options, hrt, rotation, z_index, comment, link

7.3 Image

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

  • x
  • x_anchor

Optional: format, rotation, z_index, comment, link

data supports:

  • Filename (loaded from resource layer)
  • Base64 data

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": {
    "value": "Fruit",
    "row_span": 2,
    "col_span": 1,
    "style": {
      "text": { "font_weight": "bold" }
    },
    "link": {
      "target": { "type": "url", "url": "https://example.com" }
    }
  }
}

Complex cell fields:

FieldTypeRequiredDescription
valuestring | number | boolean | nullNoCell value
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"
  • 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

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"

7.4.5 Grid

json
{
  "frame": {
    "color": "#111111",
    "width": 0.3,
    "compound": { "kind": "double", "gap": 0.3 }
  },
  "horizontal": { "color": "#D1D5DB", "width": 0.2 },
  "vertical": false
}

Fields:

  • frame?: false | StrokeStyle
  • horizontal?: false | StrokeStyle
  • vertical?: false | StrokeStyle

Note:

  • grid.frame, grid.horizontal, grid.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:
    • color defaults to #000000
    • width defaults to 1.0
  • 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

CodeTrigger
API-001Invalid JSON payload
API-002Validation failure
API-101Missing or malformed Authorization header
API-102Authentication failed
API-500Internal system failure
API-501PDF rendering failure

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
  • x and x_anchor provided simultaneously