Skip to content

Document model

A kove document is a JSON object. You describe what you want (a header, a table, some totals) and kove decides how it looks and how it splits across pages. You never write CSS or page-break: you just declare the content.

{
"page": { "size": "A4", "margin": "18mm" },
"header": { "repeat": true, "text": "My company · Document" },
"footer": { "repeat": true, "pageNumbers": "Page {page} of {pages}" },
"body": [ /* blocks in order */ ]
}
Field Type Required Description
page object no Page size and margins.
header object no Running header (repeats on every page).
footer object no Running footer with page numbers.
body block[] yes The list of document blocks, in order.

Only body is required. Everything else has sensible defaults.

Defines the canvas. size takes A4 (default), Letter, Legal, A5, A3. margin is a CSS length (for example 18mm, default 18mm).

{ "size": "A4", "margin": "18mm" }

Text that repeats at the top of every page, so you do not have to repeat it yourself. That is what it gives you: the document identity on each sheet.

{ "repeat": true, "text": "Acme Inc. · Invoice F-2026-001" }

The text is escaped (no raw HTML, for safety). repeat defaults to true.

Section titled “footer · running footer with page numbers”

A footer repeated on every page with a page-number template. pageNumbers takes {page} and {pages}.

{ "repeat": true, "pageNumbers": "Page {page} of {pages}" }

This gives you “Page X of Y” without computing anything: kove knows the total page count after it lays out the pages.

Each item in body (and in section.body) is a block with a type. These are all of them.

{ "type": "heading", "text": "Period summary", "level": 2 }

level can be 1 (default), 2, or 3. A heading is never left alone at the bottom of a page: kove keeps it with the content that follows, so you do not get stranded titles.

{ "type": "text", "text": "Payment due 30 days from the issue date." }

Regular text. It flows and splits across pages naturally.

Document metadata (customer, date, tax ID, and so on) as a list of pairs.

{
"type": "fields",
"items": [
{ "label": "Customer", "value": "Globex Corp" },
{ "label": "Date", "value": "2026-06-29" },
{ "label": "Tax ID", "value": "B-12345678" }
]
}

Each item needs a label and a value (both strings). This gives you the usual invoice or quote header block without laying out a table by hand.

The main block. Typed columns and rows as objects keyed by key.

{
"type": "table",
"repeatHeader": true,
"keepRowTogether": true,
"columns": [
{ "key": "desc", "label": "Description" },
{ "key": "qty", "label": "Qty", "align": "right" },
{ "key": "total", "label": "Total", "align": "right" }
],
"rows": [
{ "desc": "Brand design", "qty": 1, "total": "1200.00" },
{ "desc": "Support (hours)", "qty": 8, "total": "480.00" }
]
}

Each column has a key, a label, and an optional align (left / center / right). Rows are objects whose keys match the column key values.

What the table handles for you across pages:

  • repeatHeader (default true): the header row repeats on every page the table covers. A 200-row table does not leave you guessing on page 4.
  • keepRowTogether (default true): a row is never split across two pages.

The subtotal / tax / total lines, usually at the end of a table.

{
"type": "totals",
"keepWithPrevious": true,
"lines": [
{ "label": "Subtotal", "value": "$2880.00" },
{ "label": "Tax 21%", "value": "$604.80" },
{ "label": "Total", "value": "$3484.80", "emphasis": true }
]
}

Each line has a label, a value, and an optional emphasis (default false, to highlight the total). keepWithPrevious (default true) keeps the totals from being stranded on a new page, away from the table they belong to.

Groups blocks under a title, optionally keeping them together.

{
"type": "section",
"title": "Terms",
"keepTogether": true,
"body": [
{ "type": "text", "text": "Payment due 30 days from issue." },
{ "type": "text", "text": "Late fee of 1% per month." }
]
}

body is a list of nested blocks (the same building blocks). With keepTogether: true (default false) the whole section does not split across pages, which is useful for legal clauses or boxes that should be seen as one piece.

{ "type": "spacer", "size": "10mm" }

size is a CSS length (default 8mm). Controlled white space between blocks.

{ "type": "divider" }

A horizontal rule. No options.

{ "type": "pageBreak" }

Forces the start of a new page. You decide the break, not the browser.

{ "type": "image", "src": "https://cdn.acme.com/logo.png", "width": "40mm", "align": "center", "alt": "Acme logo" }

src must be an https URL or a data URI (file:// and http:// are not allowed: kove fetches the resource, so this is for safety). width (CSS length), align (left/center/right), and alt are optional.

A visual signature box (line + name + label).

{ "type": "signature", "name": "Globex Corp", "label": "Approved · sign and stamp" }

name and label are optional.

You can see all of this in one real document (an invoice with a long table, totals, terms, and a signature) in the Quickstart. To use it, see HTTP API, CLI, or MCP.