Workflows
Sections & theme
The workflow section tree: every section endpoint, backgrounds, and the document's typographic theme.
Every endpoint below is under /api/v1/workflows. Read
Workflows first for the object model; read
Templates, publication & document for
capabilities, the template resource, and publication. This page covers the
section tree and the theme; Variables & data
covers variables, preview datasets, and content images.
Workflows are a private beta, available on request at [email protected]; see Workflows.
Theme
See The object model for what the theme
is and why it travels inside each published snapshot. font-weight is
deliberately not a writable property here: setting one on a heading would
flatten the bold the renderer already gives it, and on a paragraph it would
say nothing new.
Get the theme
curl https://app.doclift.io/api/v1/workflows/templates/100050/theme \
--header "X-Api-Key: <your-api-key>"
{
"theme": {
"paragraph": { "font_family": "Arial", "font_size": 12, "color": "#1a1a1a", "line_height": 1.4 },
"h1": { "font_family": "Arial", "font_size": 24, "color": "#000000" }
},
"blocks": ["paragraph", "h1", "h2", "h3"],
"properties": ["font_family", "font_size", "color", "line_height"],
"fonts": { "faces": [{ "family": "Arial", "weights": [400, 700] }], "substitutes": { "Calibri": "Liberation Sans" } },
"font_size": { "min": 6, "max": 96, "unit": "pt" },
"line_height": { "min": 1.0, "max": 2.5, "unit": null }
}
theme only carries blocks with an actual declaration: a block never set
is simply absent, not defaulted in the response. Answers even while the
edit lock is held.
| Status | Body | When |
|---|---|---|
| 200 | body above | success |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 403 | {"error": "..."} | see Shared behaviour |
Update the theme
curl https://app.doclift.io/api/v1/workflows/templates/100050/theme \
--request PATCH \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"theme": {
"h1": { "font_family": "Arial", "font_size": 24, "color": "#000000" }
}
}'
{
"theme": {
"paragraph": { "font_family": "Arial", "font_size": 12, "color": "#1a1a1a", "line_height": 1.4 },
"h1": { "font_family": "Arial", "font_size": 24, "color": "#000000" }
},
"blocks": ["paragraph", "h1", "h2", "h3"],
"properties": ["font_family", "font_size", "color", "line_height"],
"fonts": { "faces": [{ "family": "Arial", "weights": [400, 700] }], "substitutes": {} },
"font_size": { "min": 6, "max": 96, "unit": "pt" },
"line_height": { "min": 1.0, "max": 2.5, "unit": null }
}
Merge by block, not by property
A block your body does not name is left exactly as stored. A block your
body does name is written whole: sending h1 alone does not touch the
stored paragraph, but it does replace every property h1 already had
with only what you sent for h1 this time.
Any value that does not parse is dropped from the stored theme rather than
refusing the whole request. The response you get back is always the
freshly reloaded, re-normalised value, never an echo of what you sent, so
reading the response is the only way to see a drop happen. An unprovisioned
font family, a line_height carrying a unit, a font_size outside 6-96pt,
a non-hex color, an unknown block or property, and any font_weight are
all dropped this way. See
Pitfalls for the complete list and what each
drop looks like in the response.
| Status | Body | When |
|---|---|---|
| 200 | body above | success, even if some values were silently dropped |
| 400 | {"error": "..."} | body missing the theme root key |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Sections
See The object model for what a group
versus a content section carries, and why a broken reference survives
instead of being swept away. The table below is this page's own reference:
the tree is three levels deep at most (group, group, section) and
capabilities.limits.tree_depth states it.
| Kind | children | content | placements | image | running_titles |
|---|---|---|---|---|---|
| group | yes | no | no | no | root group only |
| rich_content | no | yes | no | no | no |
| image_with_variable | no | no | yes | yes | no |
List sections
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections \
--header "X-Api-Key: <your-api-key>"
{
"tree": [
{
"id": 300100,
"kind": "group",
"title": "Investor identity",
"condition": null,
"children": [
{ "id": 300101, "kind": "rich_content", "title": "Identity paragraph", "content": "<p>...</p>" }
]
}
]
}
The whole tree, ordered and nested.
| Status | Body | When |
|---|---|---|
| 200 | {"tree": [...]} | success |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 403 | {"error": "..."} | see Shared behaviour |
Show a section
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections/300101 \
--header "X-Api-Key: <your-api-key>"
{
"id": 300101,
"kind": "rich_content",
"title": "Identity paragraph",
"content": "<p>Dear <variable class=\"editor-text-variable non-editable-content editor-parsed\">investor_name</variable></p>",
"layout": "inline",
"page_break": "continue",
"repeat_over": null,
"parent_id": 300100,
"position": 0,
"condition": null,
"running_titles": null,
"placements": null,
"image_url": null
}
One node only (no tree key here). reference (an export/import-only
field) is deliberately never published: nothing reads it, so publishing it
would put a field in the contract nobody could say what to do with.
| Status | Body | When |
|---|---|---|
| 200 | section | success |
| 404 | {"error": "..."} | section belongs to another workflow, or the workflow itself is out of scope |
| 403 | {"error": "..."} | see Shared behaviour |
Create a section
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections \
--request POST \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"section": {
"kind": "group",
"title": "Investor identity",
"position": 0,
"children": [
{
"kind": "rich_content",
"title": "Identity paragraph",
"content": "<p>Dear <variable class=\"editor-text-variable non-editable-content editor-parsed\">investor_name</variable></p>"
}
]
}
}'
{
"section": { "id": 300100, "kind": "group", "title": "Investor identity", "children": ["..."] },
"tree": ["..."]
}
children is walked recursively: a group and its whole subtree can be
created in one call, down to the third level, so a caller does not replay N
creates and leave half a group behind when one of them fails. The response
carries both the created node and the whole tree, so you see the renumbered
siblings.
| Field | Type | Required | Notes |
|---|---|---|---|
| kind | string | yes | see Sections; read section.kinds from capabilities |
| title | string | yes | |
| content | string | no | rich_content only (refused on group/image_with_variable) |
| condition | object | no | {match: "all"|"any", rules: [...]} |
| layout | string | no | inline or full_page |
| page_break | string | no | continue, new_page, or own_page |
| repeat_over | string | no | name of a collection variable; refused if nested inside another repetition |
| placements | array | no | image_with_variable only; each entry accepts id, kind, variable, value, x, y, width, height, align, font_size, color, font_family, font_weight |
| running_titles | object | no | root groups only |
| parent_id | integer | no | must name a group in this same workflow, at a legal depth |
| position | integer | no | sibling order |
| children | array | no | recursive, same shape, up to the third level |
kind on a placement entry selects the placement type, not the section
kind above it: the current, authoritative list is
capabilities.placement.keys_by_kind, e.g. text and checkbox (both
carrying variable) and static_text (carrying value). It decides only
whether the entry carries variable (bound to a declared variable) or
value (a literal string); the positioning/style keys (x, y, width,
height, align, font_size, color, font_family, font_weight) apply
to every kind, and any of the geometry keys left absent is read as 0.
Whether a radio/select variable can be placed is not stated by this
reference. Read capabilities.placement.keys_by_kind live rather than
assuming a fixed list.
A misspelled placement key is dropped, not refused
Only the exact keys listed above are kept. Anything else answers 201 and
is silently absent from the stored section. The label prints as nothing at
the position it should have occupied. See
Pitfalls for this and every other write that
answers 200 while quietly doing less than asked.
Content is sanitised on write against the allow-lists capabilities publishes: an off-list tag or attribute is stripped silently, same as the misspelled placement key above.
| Status | Body | When |
|---|---|---|
| 201 | body above | success |
| 422 | {"errors": ["..."]} | blank title; content on a group or image section; placements on anything but image_with_variable; unknown/foreign/wrong-depth parent_id; nested repetition; running_titles on a non-root-group; a numbering start outside 1-100; the organization's section limit reached |
| 400 | {"error": "..."} | body missing the section root key |
| 404 | {"error": "..."} | not a workflow, archived, or another organization's |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Update a section
Same fields as create, minus the structural ones (kind, parent_id,
position are move's job). Sending repeat_over: "" is
read as "no repetition."
{
"section": { "id": 300101, "title": "Identity paragraph", "content": "<p>...</p>" },
"tree": ["..."]
}
| Status | Body | When |
|---|---|---|
| 200 | body above | success |
| 422 | {"errors": ["..."]} | same validations as create where relevant |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Move a section
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections/300101/move \
--request PATCH \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{ "section": { "parent_id": 300100, "position": 1 } }'
{
"section": { "id": 300101, "parent_id": 300100, "position": 1 },
"tree": ["..."]
}
Changes parent and/or rank; siblings on both the old and new parent are renumbered.
| Field | Type | Required | Notes |
|---|---|---|---|
| parent_id | integer | no | must name a group in this workflow at a legal depth |
| position | integer | no | sibling rank |
| Status | Body | When |
|---|---|---|
| 200 | body above | success |
| 422 | {"errors": ["..."]} | unknown parent_id, or dragging a group that already holds a group into another group |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Duplicate a section
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections/300100/duplicate \
--request POST \
--header "X-Api-Key: <your-api-key>"
{
"section": { "id": 300200, "kind": "group", "title": "Investor identity" },
"tree": ["..."]
}
Deep-copies the node and its whole subtree inside the same template, backgrounds included. The copy is appended to the tree.
| Status | Body | When |
|---|---|---|
| 201 | body above | success |
| 422 | {"errors": ["..."]} | the organization's section limit would be exceeded by the copy |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Delete a section
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections/300101 \
--request DELETE \
--header "X-Api-Key: <your-api-key>"
{ "tree": ["..."] }
Deleting a group takes its whole subtree with it. No "section" key in
the response: the node named in the URL no longer exists. Siblings after
it are renumbered.
| Status | Body | When |
|---|---|---|
| 200 | {"tree": [...]} | success |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Set a section's background
Only valid on an image_with_variable section. Two upload shapes: a
multipart upload under background[file], or a JSON body with filename,
content_type, and data (raw base64 or a full data: URI).
curl https://app.doclift.io/api/v1/workflows/templates/100050/sections/300150/background \
--request PATCH \
--header "X-Api-Key: <your-api-key>" \
--header "Content-Type: application/json" \
--data '{
"background": {
"filename": "cover.png",
"content_type": "image/png",
"data": "data:image/png;base64,<...>"
}
}'
{
"section": { "id": 300150, "kind": "image_with_variable", "image_url": "/workflows/images/9f2a1c..." },
"tree": ["..."]
}
Every upload goes through the same pipeline: re-encoded to WebP, resized to
at most 2480px (backgrounds) or 1654px (content images), and checked
against the organization's byte ceiling. Backgrounds additionally enforce a
minimum width (the organization's workflow_image_min_width, default
800px) since a background is stretched to page width; a content image is
placed at whatever size you give it, so it has no floor.
| Status | Body | When |
|---|---|---|
| 200 | body above | success |
| 422 | {"errors": ["..."]} | not an image_with_variable section; missing (no file/bytes); too_large (over the organization's byte ceiling); wrong_type (checked against the re-encoded bytes, not the declared content-type); too_narrow (under the minimum width) |
| 400 | {"error": "..."} | body missing the background root key |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |
Remove a section's background
{
"section": { "id": 300150, "kind": "image_with_variable", "image_url": null },
"tree": ["..."]
}
This sets the reference to nothing rather than changing anything: the underlying file is left in storage, never deleted, because a published snapshot has to keep rendering the way it looked the day it was published.
| Status | Body | When |
|---|---|---|
| 200 | body above | success |
| 422 | {"errors": ["..."]} | not an image_with_variable section |
| 404 | {"error": "..."} | section or workflow out of scope |
| 409 | {"error": "..."} | edit lock held |
| 403 | {"error": "..."} | see Shared behaviour |