Structured letter content (blocks)
blocks is the structured alternative to plain content: typed paragraphs, headings, key-value rows, tables with real column math, totals lines and named styles, so an agent can build an invoice or a price list instead of hand-formatting a wall of text. Every element stays inside bounded, validated rails, so a structured letter can never break DIN 5008 or the delivery clear zones.
- 1
content or blocks, never both
A letter carries its body either as content (plain text, as before) or as blocks (structured: tables, headings, totals lines). Exactly one of the two, never both and never neither. blocks is available as a field on letter_create_draft, order_send (inline) and letter_preview. An optional document-level styleDefs object names reusable styles that blocks and cells reference by name.
- 2
The vocabulary
Nine block types, all available from day one. Deliberately NOT included: explicit page-break blocks (pagination is automatic, table/totals/box already keep themselves together) and bold/italic markup inside a paragraph. Style a whole paragraph, heading, cell or line via style/styleOverride instead.
paragraph text Plain text, align left/center/right/justify, whole paragraph is ONE style (no bold/italic runs inside) heading text Heading, level 1..3 keyValue rows Label/value rows (e.g. invoice number / date), up to 40 rows table columns, rows The workhorse: up to 20 columns, 100 data rows, colspan/rowspan, format eur/date/plain, header row repeats across page breaks totals lines Label/amount lines, up to 20, emphasis for the grand total, kept together across a page break columns flows (2..3) Parallel flows of leaf blocks (+ box) box blocks (1..40) Bordered/filled container, e.g. a hint panel image attachmentId PNG/JPG, uploaded first via attachment_upload_image spacer lines Blank lines, 1..20
- 3
Money is always integer cents
Every amount is an integer in cents, never a float. 32.00 EUR is 3200, not 32 or 32.00. A table column with "format": "eur" renders the cell value (or a cell object's amountCents field) as formatted currency, e.g. 3,653.30 €. A table cell is a bare string (text), a bare integer (cents, only meaningful in an eur column), null (empty cell), or an object { text?, amountCents?, align?, colspan?, rowspan?, style?, styleOverride? } for anything beyond plain text.
- 4
styleDefs: named styles plus inline overrides
A document may carry a top-level styleDefs object, up to 24 entries: { "accent": { "font": "sans", "sizePt": 10, "color": "primary", "bold": true } }. Blocks and cells reference a style by name via the style field, or supply a small inline styleOverride with the same shape. Changing one styleDef restyles the whole document. Built-in styles (always available, even with no styleDefs object): default, bold, muted, small, accent, heading1, heading2, heading3, tableHeader, tableCell, totals, totalsEmphasis, caption. Bounds: font is default/serif/sans/mono (default is Times, matching plain-content letters), sizePt 8 to 16, color is one of primary/primaryDark/ink/muted or the grayscale gray0 to gray100, never free RGB. FrankKi checks every style for readability (contrast on white paper), an invisible style is rejected with a fix hint, never silently printed.
- 5
Limits, all in one place
Generous but hard-capped. Page count stays capped separately and is priced per page, dryRun (on order_send and letter_preview) shows the page count and price before a large table actually gets sent.
Blocks per document (nested children counted) 200 Table rows 100 Table columns 20 Characters per table cell 2000 Images per document 10 Serialized payload 256 KB Characters per paragraph 4000 Characters per heading 200 Characters per image caption 300 Label characters (keyValue/totals/table column) 120 Value characters (keyValue) 300 Amount (cents) -1,000,000,000 .. 1,000,000,000 Font size (styleDef) 8 .. 16pt Named styles per document 24
- 6
Errors are machine-fixable
An invalid blocks payload never comes back as a bare Zod error. It is a VALIDATION_ERROR (400) with a details array of { path, code, message, hint } entries, e.g. "blocks[2].rows[4].sum: expected integer cents" with a hint that spells out the fix. The intent is a one-round repair loop: read the hint, fix the exact path, resend. The full description of VALIDATION_ERROR lives in the error catalog.
- 7
The crafting loop
The fastest start is not a blank page but a curated template. template_list marks them curated: true, and block-based ones additionally hasBlocks: true. template_get on one returns its full blocksTemplate plus styleDefs to copy and modify. Then call letter_preview with the modified blocks: it returns the first pages as an image directly in the result, plus a page count and a cost estimate, without charging or sending anything. Adjust blocks, call letter_preview again, until the layout is right. A round takes seconds. Then save it with template_save as a draft and release it with template_release, or for a one-off letter go straight to letter_create_draft and order_send. Letterhead, footer and colors come from your letter design and are set around your blocks, so do not redraw them yourself.
letter_create_draft { "subject": "Rechnung Juni 2026", "blocks": [ { "type": "heading", "text": "Rechnung" }, { "type": "table", "columns": [ { "key": "text", "label": "Artikel", "width": "grow", "align": "left" }, { "key": "sum", "label": "Summe netto", "width": "auto", "align": "right", "format": "eur" } ], "rows": [ { "text": "Beratung Website Neugestaltung", "sum": 32000 } ] }, { "type": "totals", "lines": [ { "label": "Rechnungssumme", "amountCents": 32000, "emphasis": true } ] } ], "senderAddressId": "<senderAddressId>", "recipientAddressInline": { "name": "Mandant GmbH", "street": "Musterstr.", "houseNumber": "1", "zip": "12345", "city": "Musterstadt", "country": "DE" } } - 8
Block templates: the same blocks, saved
A template is not a second content model. blocksTemplate is the SAME blocks array, with {{tokens}} left in the text fields, plus the same styleDefs map. A template carries contentTemplate (plain text) OR blocksTemplate (structured), never both, mirroring the letter itself. The curated FrankKi library covers Rechnung klassisch, Rechnung modern, Mahnung, Zahlungserinnerung, Preisliste, Honorarnote, Angebot and Bericht mit Tabellen: every partner may read them, none may overwrite them, they exist to be copied. Merge fields are typed: text (the default), date (ISO YYYY-MM-DD), number, currency (integer cents) and rows. A rows field is the variable-length half of a template: the table block that draws it names it via rowsFrom instead of carrying fixed rows, and on apply you pass a list of objects keyed by the column keys of that table. Every rows field must be bound by exactly one table and vice versa; template_save rejects anything else with the exact path. Drafts are free: creating, overwriting and previewing them as often as you like costs nothing and mails nothing. Only template_release is gated, and only by the four-eyes rule: with more than one active user in your account a human releases it in the dashboard and the response carries the link.
// template_save -> blocksTemplate { "type": "table", "columns": [ { "key": "text", "label": "Leistung", "width": "grow" }, { "key": "sum", "label": "Betrag", "width": "auto", "align": "right", "format": "eur" } ], "rowsFrom": "positionen" } // template_save -> mergeFields [ { "key": "positionen", "type": "rows", "label": "Positionen", "required": true } ] // template_apply_with_merge_fields -> mergeValues { "positionen": [ { "text": "Beratung Juni", "sum": 32000 } ] } - 9
Three worked agent flows
First, "Rechnung fuer Juni an Mandant X" ("invoice for June to client X"): mandant_search finds the client, document_create turns the line items (integer cents) into the invoice, re-computes net, VAT rates and gross, checks the mandatory details under § 14 UStG and optionally draws the number from your sequence, order_send sends it. Instead of order_send, approval_submit puts the letter in front of a human first. Second, "Baue mir eine Preisliste im Stil meines Briefdesigns" ("build me a price list in the style of my letter design"): the crafting loop from step 7. Third, "Mahne Rechnung 2026-014 an" ("send a dunning letter for invoice 2026-014"): document_list finds the invoice, document_get returns a ready-made referencePrefill block that you paste unchanged into document.references of a document_create with documentType mahnung, then order_send. The dunning letter then carries the reference to the invoice and document_list finds both sides of the chain.
"Invoice for June to client X" mandant_search -> document_create (rechnung) -> order_send "Build me a price list in the style of my letter design" template_list -> template_get (curated: true) -> modify blocks -> letter_preview -> adjust -> letter_preview -> template_save -> template_release "Send a dunning letter for invoice 2026-014" document_list -> document_get (returns referencePrefill) -> document_create (mahnung, references) -> order_send
- 10
The full reference via MCP
The MCP resource frankki://blocks-guide serves the same reference machine-readably straight from the protocol (resources/list, resources/read): the vocabulary, all table options, the same limits, the template and rows rules, a compact example and the same three flows. An agent without browser access needs nothing from this page. For the document half (invoice, dunning letter, payment reminder, credit note, ZUGFeRD and XRechnung) the e-invoicing guide sits next to it.
See the tool reference →Documents and e-invoicing (ZUGFeRD) →See the error catalog →
Trust and live status
Machine-readable service facts and the current operational status of the partner interface are publicly available at all times, no login needed.