The document model
A template is one JSON document. The visual editor saves it, the element tools edit it, and you can write it whole. Knowing its shape is what lets an agent build a template in one piece instead of a hundred calls.
The format is madoo.design-document/2.0. Its JSON Schema is served at
GET /api/v1/json-schemas/madoo.design-document/2.0 (MCP: get_json_schema). Read a real document with
get_design_template view content (REST: GET /api/v1/design-templates/{id}/draft/content).
The smallest useful document
Section titled “The smallest useful document”This document is accepted as it is: a coloured page, a band, a title field and a photo field. Properties you do not write take their defaults.
{ "schemaVersion": "2.0", "pages": [ { "id": "7f1c2a10-0000-4000-8000-000000000001", "name": "Poster", "width": 595, "height": 842, "pageFormat": "A4", "backgroundColor": "#f6f1e7", "elements": [ { "$type": "rectangle", "id": "7f1c2a10-0000-4000-8000-000000000010", "name": "Header band", "left": 0, "top": 0, "width": 595, "height": 300, "fill": "#23493a" }, { "$type": "text", "id": "7f1c2a10-0000-4000-8000-000000000011", "name": "Title", "left": 40, "top": 60, "width": 330, "height": 110, "text": "Books in the courtyard", "fontFamily": "Playfair Display", "fontSize": 40, "fontWeight": "bold", "fill": "#ffffff", "placeholder": { "id": "7f1c2a10-0000-4000-8000-000000000101", "name": "Event title", "code": "title", "placeholderType": "text", "required": true } }, { "$type": "image", "id": "7f1c2a10-0000-4000-8000-000000000012", "name": "Photo", "left": 390, "top": 40, "width": 165, "height": 220, "src": "", "fitMode": "fill", "placeholder": { "id": "7f1c2a10-0000-4000-8000-000000000102", "name": "Photo", "code": "photo", "placeholderType": "image", "required": true, "fitMode": "fill" } } ] } ]}A complete example with a flowing layout, a repeated list, a formatted price, a conditional badge, a link and a sample set is the event poster.
The document
Section titled “The document”The root object has schemaVersion (always "2.0"), pages, and optionally masterPages, metadata (title,
author, subject, keywords of the PDF), sampleSets, styles and components. Unknown properties are errors,
anywhere in the document.
A page has an id, a name, a size and its elements.
- Size.
widthandheightare in PDF points (1 pt = 1/72 inch) and they decide the size: A4 is 595 × 842, US Letter 612 × 792, a landscape A4 certificate 842 × 595, a 4:5 social post 540 × 675.pageFormatis only a label (A4,Letter,Custom). The page commands of REST and MCP accept the named formatsa3,a4,a5,letter,legal,tabloid,squarewith an orientation, orcustomwith a width and a height. - Background.
backgroundColoris the base;backgroundPaintcan add a linear or radial gradient; abackgroundImage(withfitModecover,containorstretch, a focal point and a scale) sits above both. - Height that follows the content.
fitHeightwith abottomMarginmakes a page end a fixed distance below its lowest printed element — never taller than the drawn height. It suits a one-page summary whose length varies. - A document has 1 to 100 pages. The order of
pagesis the order of the PDF.
Coordinates
Section titled “Coordinates”Every element has left, top, width and height in points, measured from the top-left corner of the page.
Children of a container — a group, a flowing Layout, a row of a repeated list — are positioned relative to their
container. An element can also be rotated (angle), mirrored (flipX, flipY), made translucent (opacity)
or hidden (visible: false).
Elements are painted in array order: the first element of elements is at the back, the last is in front.
Elements
Section titled “Elements”Every element has an $type, a unique id and a name. The types are:
$type |
What it is |
|---|---|
text |
A text box: font, size, weight, color, alignment, line height, letter spacing, optionally rich text with mixed styles and bullet or numbered lists |
image |
A picture: from a URL, from Madoo storage or from a field; fitMode fit, fill or stretch; an optional frame shape and mask |
rectangle, circle, ellipse, line, path |
Vector shapes with fill and stroke (solid or gradient), rounded corners, dashes, arrow heads; path takes standard SVG path data |
svg_artwork |
Imported SVG artwork kept as vector, with its colors exposed as a palette you can recolor |
group |
Elements that move together |
layout_region |
A flowing Layout: its children are arranged vertically, horizontally or in a grid, with padding and gaps; a text that grows pushes what follows it |
repeat_region |
A repeated list: one row template drawn once per item of a JSON list field, vertically, horizontally or as a grid |
component_instance |
A copy of a reusable component defined in components, with per-copy overrides |
folder |
A layer folder: it only organises the layers panel and does not change what prints |
Each of them is covered in depth by the technique pages. Any element can carry a field (placeholder), a
condition that decides whether it prints, and a link that makes it clickable in the PDF — see
Fields and data.
Master pages
Section titled “Master pages”A master page holds what several pages share: a letterhead, a footer, a page number. Masters live in
masterPages (up to 32, not counted among the 100 pages) and have the same shape as pages.
- A master applies only to pages of the same size.
- A page links to a master with
masterPageId, draws the master below or above its own elements (masterLayer:underlayoroverlay), and can use the master’s background (useMasterBackground). - Masters can be assigned automatically: a master’s
automaticRuleisall,oddoreven, and a page whosemasterAssignmentisautomaticreceives the master that matches its final page number.manualandnoneopt out. - A master holds static content only — no fields, no conditions, no repeated lists.
Page numbers are tokens inside a text: {{page}} (the page number), {{pages}} (the pages of the document) and
{{sequencePages}} (the pages of the current numbering sequence). They are resolved after lists have expanded, so
they are right even when a list adds pages. A page can start a new sequence (numberingStart) in arabic,
roman_upper or roman_lower (numberingStyle), or hide its number (hidePageNumber).
Identifiers
Section titled “Identifiers”Every page, master, element, field, style and sample set has an id — a GUID unique in the document. Write
them yourself when you create a document; keep them when you rewrite one, so that edits and history stay
connected. Field codes (title, price) are what the data refers to; element names are for people.
Limits
Section titled “Limits”| Limit | |
|---|---|
| Pages | 1–100 |
| Master pages | 32 |
| Elements | 1,000 per page or master, 10,000 in the document |
| Nesting | groups 16 levels, Layouts 8, repeated lists 2 |
| Repeated list | 1–100 items per list |
| Components | 200, each up to 500 elements |
| Rich text | 1,000 paragraphs, 10,000 runs, 10,000 characters per text |
A single render has its own ceilings (200 output pages, 25,000 resolved elements, 20 MiB per image, 100 MiB of images in total).
Validation
Section titled “Validation”A document is validated as a whole every time it is saved, by the same rules as an editor save. A rejected document returns every problem with its JSON path and a code, for example:
{ "code": "content_invalid", "errors": [ { "field": "element:3b9f…024.link", "message": "DESIGN_LINK_INVALID: The link reads {event_id}, but the document has no field with that code." } ] }Fix all of them and save again. Common causes: an unknown property, a duplicated id, a field code that is not a
valid identifier, a list key used outside its list, a link or condition that reads a field the document does not
have, a value outside its limits.
Naming: the document and the API
Section titled “Naming: the document and the API”The document uses camelCase (fitMode, placeholderType); REST, MCP and agent commands use snake_case
(fit_mode, placeholder_type). A few names differ beyond the case — write the document form in documents:
| In the document | In REST / MCP commands |
|---|---|
number format numberStyle |
style |
paintOrder: fill_then_stroke, stroke_then_fill |
paint_order: fill_stroke, stroke_fill |