Skip to content

O3O DocBuilder and conversion

o3oscript for text documents

Every operation of a type = text script: page setup, text style, header and footer, formatted runs, headings, paragraphs, lists, tables, images and page breaks, each with a parameter table and an example.

On this page

A type = "text" script builds a text document. Content goes in the body array (1 to 20,000 blocks) in reading order; each block has a type of heading, paragraph, list, table, image or page_break. Save formats: docx, odt, pdf.

Paper and margins: page#

The page object

  • sizeA3 | A4 | A5 | Letter | Legal | objectoptionalDefault: A4
    A named paper size, or {"width", "height"} in mm (50 to 2000).
  • orientationportrait | landscapeoptionalDefault: portrait
    Orientation.
  • marginobjectoptionalDefault: 20 mm each side
    Page margins.
  • margin.top / right / bottom / leftnumber 0–2000 (mm)optionalDefault: 20
    Margin of each side.

The style object (default text style)

  • fontstringoptional
    Default font.
  • sizenumber 4–400 (pt)optional
    Default font size.
  • color#RRGGBBoptional
    Default text colour.
  • line_spacingnumber 50–300 (%)optional
    Line spacing; 100 is single.
JSONA4, 30 mm left margin, 13 pt serif text
{
  "page": {
    "size": "A4",
    "orientation": "portrait",
    "margin": {
      "top": 20,
      "right": 15,
      "bottom": 20,
      "left": 30
    }
  },
  "style": {
    "font": "Liberation Serif",
    "size": 13,
    "color": "#000000",
    "line_spacing": 120
  }
}

The header and footer are a single line applied to every page. content mixes plain strings, run objects and auto-updating fields.

The header or footer object

  • contentarray, 1–50 itemsrequired
    Each item is a string (≤ 1000 characters), a run object, or a field {"field": ...}.
  • alignleft | center | rightoptionalDefault: center
    Alignment.
  • sizenumber 4–400 (pt)optional
    Font size.
  • color#RRGGBBoptional
    Text colour.

Auto-updating field

  • fieldpage_number | page_count | daterequired
    page_number: current page; page_count: total pages; date: build date as dd/MM/yyyy.
JSONCompany header and page-numbered footer
{
  "header": {
    "content": [
      {
        "text": "CÔNG TY CỔ PHẦN O3O",
        "bold": true
      },
      "  ·  Tài liệu nội bộ"
    ],
    "align": "left",
    "size": 9,
    "color": "#555555"
  },
  "footer": {
    "content": [
      "Trang ",
      {
        "field": "page_number"
      },
      "/",
      {
        "field": "page_count"
      },
      "  ·  Ngày in: ",
      {
        "field": "date"
      }
    ],
    "align": "right",
    "size": 9
  }
}

Formatted runs: run#

Everything that accepts runs (headings, paragraphs, list items, table cells) and header or footer content uses the same run object: a stretch of text sharing one format. When a block has both text and runs, runs wins.

The run object

  • textstring, ≤ 100,000required
    The run's text.
  • boldbooleanoptional
    Bold.
  • italicbooleanoptional
    Italic.
  • underlinebooleanoptional
    Underline.
  • strikebooleanoptional
    Strikethrough.
  • superscriptbooleanoptional
    Superscript.
  • subscriptbooleanoptional
    Subscript.
  • color#RRGGBBoptional
    Text colour.
  • background#RRGGBBoptional
    Background (highlight) colour.
  • sizenumber 4–400 (pt)optional
    Font size.
  • fontstringoptional
    Font name.
  • linkURL, ≤ 2048optional
    Turns the run into a hyperlink.
JSONA paragraph mixing formats
{
  "type": "paragraph",
  "runs": [
    {
      "text": "Hạn nộp hồ sơ: "
    },
    {
      "text": "30/09/2026",
      "bold": true,
      "color": "#C00000"
    },
    {
      "text": ". Hướng dẫn tại "
    },
    {
      "text": "office.o3o.vn",
      "link": "https://office.o3o.vn",
      "underline": true,
      "color": "#1D55B8"
    },
    {
      "text": ". Ký hiệu hoá học của nước là H"
    },
    {
      "text": "2",
      "subscript": true
    },
    {
      "text": "O."
    }
  ]
}

Headings: heading#

The heading block

  • type"heading"required
    Block type.
  • levelinteger 1–6required
    Heading level.
  • textstring, ≤ 2000optional
    Plain text. Either text or runs is required.
  • runsarray of runs, 1–200optional
    Formatted text.
  • alignleft | center | right | justifyoptional
    Alignment.
  • color#RRGGBBoptional
    Text colour.
  • page_break_beforebooleanoptionalDefault: false
    Start a new page before the heading.
JSONA level-2 heading
{
  "type": "heading",
  "level": 2,
  "text": "Điều 3. Giá trị hợp đồng",
  "color": "#1D55B8"
}

Headings map to the Heading 1 to Heading 6 paragraph styles, so they appear in the editor's navigator and become bookmarks in PDF output.

Paragraphs: paragraph#

The paragraph block

  • type"paragraph"required
    Block type.
  • textstring, ≤ 100,000optional
    Plain text; a \n becomes a line break inside the paragraph. Either text or runs is required.
  • runsarray of runs, 1–2000optional
    Formatted text.
  • alignleft | center | right | justifyoptional
    Alignment.
  • spacing_beforenumber (mm)optional
    Space before the paragraph.
  • spacing_afternumber (mm)optional
    Space after the paragraph.
  • line_spacingnumber 50–300 (%)optional
    Line spacing of this paragraph.
  • indent_leftnumber (mm)optional
    Left indent.
  • indent_rightnumber (mm)optional
    Right indent.
  • indent_firstnumber −200–200 (mm)optional
    First-line indent; a negative value makes a hanging indent.
  • keep_with_nextbooleanoptionalDefault: false
    Keep with the next paragraph.
  • page_break_beforebooleanoptionalDefault: false
    Start a new page before it.
JSONJustified paragraph, 10 mm first-line indent, line break
{
  "type": "paragraph",
  "text": "Căn cứ Bộ luật Dân sự năm 2015;\nCăn cứ nhu cầu và khả năng của hai bên.",
  "align": "justify",
  "indent_first": 10,
  "spacing_after": 3,
  "line_spacing": 130
}

Lists: list#

The list block

  • type"list"required
    Block type.
  • stylebullet | numberoptionalDefault: bullet
    Bullets or numbering.
  • itemsarray, 1–5000 itemsrequired
    Each item is a string (≤ 10,000 characters) or an item object.

The list item object

  • textstring, ≤ 10,000optional
    Plain text. Either text or runs is required.
  • runsarray of runs, 1–200optional
    Formatted text.
  • itemsarray, 1–1000 itemsoptional
    Sub-list, same style as the parent.
JSONNumbered list with sub-items
{
  "type": "list",
  "style": "number",
  "items": [
    "Bên A thanh toán trong 7 ngày làm việc.",
    {
      "text": "Bên B có trách nhiệm:",
      "items": [
        "Bàn giao token bản quyền",
        {
          "runs": [
            {
              "text": "Hỗ trợ kỹ thuật "
            },
            {
              "text": "trong giờ hành chính",
              "italic": true
            }
          ]
        }
      ]
    },
    "Hai bên cùng bảo mật thông tin."
  ]
}

Tables: table#

The table block

  • type"table"required
    Block type.
  • columnsarray, 1–63 columnsoptional
    Each column is {"width", "align"}: width is a relative width from 1 to 1000 (default 1).
  • headerarray of cells, 1–63optional
    Header row.
  • rowsarray of rows, 1–10,000required
    Each row is an array of 1 to 63 cells.
  • borderfalse | border objectoptional
    false: no lines; an object: draw every line, outer border and inner grid.
  • header_fill#RRGGBBoptional
    Header row background.
  • header_color#RRGGBBoptional
    Header row text colour.
  • header_boldbooleanoptionalDefault: true
    Bold header row.
  • zebra_fill#RRGGBBoptional
    Background of even data rows.
  • repeat_headerbooleanoptionalDefault: true
    Repeat the header row on each page.
  • width_percentnumber 10–100optionalDefault: 100
    Table width as a percentage of the text area.
  • alignleft | center | rightoptionalDefault: left
    Table position when width_percent is below 100.
  • sizenumber 4–400 (pt)optional
    Font size inside the table.
  • cell_paddingnumber 0–20 (mm)optionalDefault: 1.5
    Cell padding.
  • spacing_afternumber (mm)optional
    Space after the table.

Table cell as an object

  • textstring, ≤ 20,000optional
    Plain text. Either text or runs is required.
  • runsarray of runs, 1–200optional
    Formatted text.
  • alignleft | center | right | justifyoptional
    Horizontal alignment.
  • valigntop | middle | bottomoptional
    Vertical alignment.
  • boldbooleanoptional
    Bold.
  • italicbooleanoptional
    Italic.
  • color#RRGGBBoptional
    Text colour.
  • fill#RRGGBBoptional
    Cell background.

The border object

  • widthnumber 0.25–6 (pt)optionalDefault: 0.5
    Line width.
  • color#RRGGBBoptional
    Line colour.
JSONThree-column table with a total row
{
  "type": "table",
  "columns": [
    {
      "width": 1,
      "align": "center"
    },
    {
      "width": 5
    },
    {
      "width": 2,
      "align": "right"
    }
  ],
  "header": [
    "STT",
    "Hạng mục",
    "Số tiền (đồng)"
  ],
  "rows": [
    [
      1,
      "Thiết kế giao diện",
      "8.000.000"
    ],
    [
      2,
      "Lập trình và kiểm thử",
      "24.000.000"
    ],
    [
      {
        "text": "Cộng",
        "bold": true,
        "align": "right"
      },
      null,
      {
        "text": "32.000.000",
        "bold": true,
        "fill": "#EAF1FB"
      }
    ]
  ],
  "border": {
    "width": 0.5,
    "color": "#7F7F7F"
  },
  "header_fill": "#1D55B8",
  "header_color": "#FFFFFF",
  "zebra_fill": "#F7F9FC",
  "size": 11,
  "spacing_after": 4
}
Coming soon

Merged cells in text tables.

Images: image#

The image block

  • type"image"required
    Block type.
  • srcobjectrequired
    Image source: {"url"} or {"base64", "mime"}.
  • widthnumber (mm)optional
    Width. With only one side given, the other follows the original ratio.
  • heightnumber (mm)optional
    Height.
  • alignleft | center | rightoptionalDefault: left
    Image alignment.
  • captionstring, ≤ 1000optional
    Caption: a centred italic paragraph right below the image.
  • altstring, ≤ 1000optional
    Alternative text for screen reader users.

Image source src

  • urlhttp/https URL, ≤ 2048optional
    DocBuilder downloads the image under the SSRF rules. Use url OR base64.
  • base64stringoptional
    Standard Base64 image data, WITHOUT a data: prefix, at most 10 MB decoded.
  • mimeimage/png | image/jpeg | image/gif | image/svg+xmloptionalDefault: image/png
    Image type when using base64.
JSONImage from a URL with a caption
{
  "type": "image",
  "src": {
    "url": "https://example.com/images/so-do-to-chuc.png"
  },
  "width": 120,
  "align": "center",
  "caption": "Hình 1. Sơ đồ tổ chức",
  "alt": "Sơ đồ tổ chức công ty"
}
JSONEmbedded base64 image
{
  "type": "image",
  "src": {
    "base64": "iVBORw0KGgoAAAANSUhEUgAAAKAAAABaCAIAAACwpMoFAAAAsUlEQVR42u3RMQ0AIAwAwapgIYhhZccMyhiRhoA6aC55BX8x9lXhwgLAAizAAizAAizAgAVYgAVYgAVYgAELsAALsAALsAALMGABFmABFmABVgJ+q6twgAELsAALsAALsAADFmABFmABFmABBizAAizAAizAAizAgAVYgAVYgAVYGbidqcIBBizAAizAAizAAgxYgAVYgAVYgAUYsAALsAALsAALsAADFmABFmABFmClPpawm2fxo+U6AAAAAElFTkSuQmCC",
    "mime": "image/png"
  },
  "height": 20,
  "alt": "Ba dải màu"
}

Page breaks: page_break#

JSONPage break
{
  "type": "page_break"
}

A page_break block has only the type key. To make a heading or paragraph always start on a new page, set page_break_before: true on that block instead.

Complete example: a quotation#

bao-gia.json builds a two-page quotation: header, numbered footer, headings, formatted runs, a table with a total row, nested lists and a page break. The script has 28 units.

JSONbao-gia.json
{
  "o3oscript": 1,
  "type": "text",
  "meta": {
    "title": "Báo giá O3O Office Online",
    "author": "Công ty Cổ phần O3O",
    "subject": "Báo giá bản quyền theo số kết nối",
    "keywords": [
      "báo giá",
      "O3O Office Online"
    ],
    "lang": "vi-VN"
  },
  "page": {
    "size": "A4",
    "orientation": "portrait",
    "margin": {
      "top": 20,
      "right": 20,
      "bottom": 20,
      "left": 25
    }
  },
  "style": {
    "font": "Liberation Sans",
    "size": 11,
    "line_spacing": 115
  },
  "header": {
    "content": [
      {
        "text": "O3O Office Online",
        "bold": true,
        "color": "#1D55B8"
      },
      "  ·  Báo giá số BG-2026-0917"
    ],
    "align": "left",
    "size": 9
  },
  "footer": {
    "content": [
      "Trang ",
      {
        "field": "page_number"
      },
      " / ",
      {
        "field": "page_count"
      }
    ],
    "align": "center",
    "size": 9,
    "color": "#666666"
  },
  "body": [
    {
      "type": "heading",
      "level": 1,
      "text": "BÁO GIÁ BẢN QUYỀN",
      "align": "center",
      "color": "#1D55B8"
    },
    {
      "type": "paragraph",
      "align": "center",
      "spacing_after": 6,
      "runs": [
        {
          "text": "Số: BG-2026-0917",
          "italic": true
        },
        {
          "text": "  ·  Ngày lập: 21/09/2026",
          "italic": true
        }
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "1. Thông tin khách hàng"
    },
    {
      "type": "paragraph",
      "runs": [
        {
          "text": "Kính gửi: ",
          "bold": true
        },
        {
          "text": "Công ty TNHH Thương mại và Dịch vụ An Phát"
        }
      ]
    },
    {
      "type": "paragraph",
      "spacing_after": 4,
      "runs": [
        {
          "text": "Địa chỉ: ",
          "bold": true
        },
        {
          "text": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
        }
      ]
    },
    {
      "type": "heading",
      "level": 2,
      "text": "2. Chi tiết báo giá"
    },
    {
      "type": "paragraph",
      "spacing_after": 3,
      "text": "Đơn giá áp cho toàn bộ số kết nối của đơn hàng. Một kết nối là một phiên soạn thảo đồng thời."
    },
    {
      "type": "table",
      "columns": [
        {
          "width": 1,
          "align": "center"
        },
        {
          "width": 6
        },
        {
          "width": 2,
          "align": "right"
        },
        {
          "width": 2,
          "align": "right"
        },
        {
          "width": 3,
          "align": "right"
        }
      ],
      "header": [
        "STT",
        "Hạng mục",
        "Số kết nối",
        "Đơn giá (USD)",
        "Thành tiền (USD)"
      ],
      "rows": [
        [
          1,
          "O3O Office Online, bản doanh nghiệp, thuê bao 1 năm",
          120,
          "5,00",
          "600,00"
        ],
        [
          2,
          "Hỗ trợ triển khai và đấu nối Nextcloud",
          null,
          null,
          "0,00"
        ],
        [
          {
            "text": "Tổng cộng",
            "bold": true,
            "align": "right"
          },
          null,
          null,
          null,
          {
            "text": "600,00",
            "bold": true,
            "fill": "#EAF1FB"
          }
        ]
      ],
      "border": {
        "width": 0.5,
        "color": "#7F7F7F"
      },
      "header_fill": "#1D55B8",
      "header_color": "#FFFFFF",
      "header_bold": true,
      "zebra_fill": "#F7F9FC",
      "repeat_header": true,
      "size": 10,
      "spacing_after": 4
    },
    {
      "type": "heading",
      "level": 2,
      "text": "3. Điều khoản"
    },
    {
      "type": "list",
      "style": "number",
      "items": [
        "Báo giá có hiệu lực 30 ngày kể từ ngày lập.",
        "Bản quyền được cấp dưới dạng token ký số, nạp vào máy chủ của khách hàng.",
        {
          "text": "Phạm vi của bản doanh nghiệp:",
          "items": [
            "Số kết nối đồng thời theo đơn hàng",
            "API DocBuilder đầy đủ",
            "Cập nhật trong suốt thời hạn thuê bao"
          ]
        }
      ]
    },
    {
      "type": "paragraph",
      "spacing_before": 8,
      "align": "right",
      "runs": [
        {
          "text": "ĐẠI DIỆN BÊN BÁN",
          "bold": true
        }
      ]
    },
    {
      "type": "paragraph",
      "align": "right",
      "runs": [
        {
          "text": "(Ký, ghi rõ họ tên và đóng dấu)",
          "italic": true,
          "size": 9
        }
      ]
    },
    {
      "type": "page_break"
    },
    {
      "type": "heading",
      "level": 2,
      "text": "Phụ lục: Cách đếm kết nối"
    },
    {
      "type": "list",
      "style": "bullet",
      "items": [
        "Một người mở ba tài liệu ở chế độ sửa được tính là ba kết nối.",
        "Phiên chỉ đọc và các yêu cầu tới DocBuilder không được tính.",
        "Khi chạm trần, phiên đang mở không bị ngắt và thao tác lưu không bị chặn."
      ]
    }
  ],
  "save": [
    {
      "format": "docx",
      "filename": "bao-gia.docx"
    }
  ]
}
Build the quotation as docx
curl -sS http://localhost:8080/v1/build \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @bao-gia.json \
  -o bao-gia.docx -w "HTTP %{http_code}\n"
import { readFile, writeFile } from "node:fs/promises";

const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };

const script = JSON.parse(await readFile("bao-gia.json", "utf8"));

const res = await fetch(`${BASE_URL}/v1/build`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify(script),
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("bao-gia.docx", Buffer.from(await res.arrayBuffer()));
import json

import requests

BASE_URL = "http://localhost:8080"
HEADERS = {"Authorization": "Bearer O3O_DEMO_KEY"}

with open("bao-gia.json", encoding="utf-8") as f:
    script = json.load(f)

r = requests.post(f"{BASE_URL}/v1/build", headers=HEADERS, json=script, timeout=90)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bao-gia.docx", "wb") as fh:
    fh.write(r.content)
<?php
$payload = file_get_contents("bao-gia.json");

$ch = curl_init("http://localhost:8080/v1/build");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY", "Content-Type: application/json"],
    CURLOPT_POSTFIELDS => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 90,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($body === false || $status >= 400) {
    throw new RuntimeException("HTTP $status: " . ($body === false ? curl_error($ch) : $body));
}
file_put_contents("bao-gia.docx", $body);
using System.Net.Http.Headers;
using System.Text;

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(90) };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");

var json = await File.ReadAllTextAsync("bao-gia.json");
using var res = await http.PostAsync("http://localhost:8080/v1/build",
    new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("bao-gia.docx", await res.Content.ReadAsByteArrayAsync());