Skip to content

O3O DocBuilder and conversion

Template filling

POST /v1/template/render fills JSON data into a docx or odt template written by your users: {{name}} fields, nested paths, table row loops; returns docx, odt or PDF. Enterprise only.

On this page

Business staff write the template in a familiar editor and place {{field_name}} tags where data goes; your system only sends JSON data. Changing the wording or layout of a template needs no code change.

POST/v1/template/render

Fill JSON data into a docx or odt template.

Auth: BearerCommunity (not included in Community)Enterprise

Writing a template#

A template is an ordinary docx or odt file. Places to fill are typed as plain text inside double curly braces. Replacement covers the body, tables, headers and footers; the text formatting at the tag position (font, size, bold, colour) is kept.

SyntaxMeaning
{{ten}}Replaced by data.ten. Field names match [A-Za-z_][A-Za-z0-9_]*; spaces next to the braces are allowed, e.g. {{ ten }}.
{{khach.ten}}Follows a path into nested objects.
{{#hang}}{{/hang}}Repeats a table row for the array data.hang, see below.
{{ten}} trong hàng lặpLooked up in the current element first, then at the root of data.
{{@index}}Position inside a row loop, starting at 1.

How values are printed#

Value in dataPrinted as
StringAs is. A \n becomes a line break inside the paragraph.
IntegerNo decimals: 20.
Decimal numberUp to 6 significant digits.
true, false, Không (Vietnamese for yes and no).
nullEmpty string.
Array or object in a plain fieldError 422 template_error.

Table row loops#

Put {{#hang}} in the FIRST cell and {{/hang}} in the LAST cell of the SAME table row. That row is copied once per element of the data.hang array and both tags are removed; an empty array removes the template row. The header row and the other rows of the table stay as they are.

STTItemQuantityAmount
{{#hang}}{{@index}}{{ten}}{{so_luong}}{{thanh_tien}}{{/hang}}

Missing fields#

options.missingField absent from data
emptyDefault. Replaced with an empty string.
keepThe tag stays in the document, handy when proofing a template.
errorReturns 422 template_error with the missing fields in detail.fields.

Templates with macros are rejected#

Template filling reads and writes the template's XML parts directly and does not load the template through LibreOffice; the other parts of the file are copied unchanged into the result, so a template with macros would carry those macros into the result. DocBuilder therefore rejects any template that contains macros, including a docm file renamed to docx, and returns 422 macro_not_allowed with a Vietnamese message. A template counts as containing macros when:

  • An OOXML (docx) file has a vbaProject.bin part, declares a macroEnabled content type in [Content_Types].xml, or has a relationship pointing at a VBA part (even a renamed one).
  • An ODF (odt) file has a Basic/ or Scripts/ folder.

The template is checked on receipt, before any job is created, so the error comes back immediately even with async = true. detail.reason gives the reason (vba_project, macro_enabled_content_type, odf_basic, odf_scripts); detail.part is the name of the offending part in the file.

Parameters#

multipart/form-data body

  • templatefilerequired
    The docx or odt template.
  • datastring (JSON)required
    The data as a JSON STRING; the root must be an object.
  • todocx | odt | pdfoptionalDefault: the template's format
    Target format.
  • filenamestring, ≤ 255optional
    Result file name.
  • optionsstring (JSON)optional
    For example {"missing": "error"}.
  • asyncbooleanoptionalDefault: false
    true: return 202 with a job right away.
  • callback_urlstring (URL)optional
    Needs the callback feature in the token.

application/json body

  • template_urlURL, ≤ 2048required
    URL of the template, subject to the SSRF rules.
  • dataobjectrequired
    The data to fill in.
  • todocx | odt | pdfoptionalDefault: the template's format
    Target format.
  • filenamestring, ≤ 255optional
    Result file name.
  • optionsobjectoptional
    Template options.
  • asyncbooleanoptionalDefault: false
    true: return 202 with a job right away.
  • callback_urlstring (URL)optional
    Needs the callback feature in the token.

The options object

  • missingempty | keep | erroroptionalDefault: empty
    How missing fields are handled.

Example: service contract#

The template hop-dong-mau.docx contains the paragraphs below (left: text typed in the template; right: the result after filling in the data further down).

Typed in the templateResult
Số: {{so_hop_dong}}Số: HD-2026-091
Hôm nay, ngày {{ngay_ky}}, tại {{dia_diem}}, chúng tôi gồm:Hôm nay, ngày 21/09/2026, tại Hà Nội, chúng tôi gồm:
BÊN A: {{ben_a.ten}}BÊN A: Công ty TNHH Thương mại và Dịch vụ An Phát
Đại diện: {{ben_a.dai_dien}}, chức vụ {{ben_a.chuc_vu}}Đại diện: Ông Nguyễn Văn Hùng, chức vụ Giám đốc
BÊN B: {{ben_b.ten}}BÊN B: Công ty Cổ phần O3O
{{#hang}}{{@index}} | {{ten}} | {{so_luong}} | {{thanh_tien}}{{/hang}}1 | O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm | 1 | 600 USD
Tổng giá trị: {{tong_tien}} ({{tong_tien_bang_chu}}).Tổng giá trị: 600 USD (Sáu trăm đô la Mỹ).
Thời hạn: {{thoi_han}}. Hỗ trợ triển khai: {{ho_tro_trien_khai}}.Thời hạn: 12 tháng kể từ ngày ký. Hỗ trợ triển khai: Có.
JSONData
{
  "so_hop_dong": "HD-2026-091",
  "ngay_ky": "21/09/2026",
  "dia_diem": "Hà Nội",
  "ben_a": {
    "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
    "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội",
    "dai_dien": "Ông Nguyễn Văn Hùng",
    "chuc_vu": "Giám đốc"
  },
  "ben_b": {
    "ten": "Công ty Cổ phần O3O",
    "dai_dien": "Bà Trần Thu Hà",
    "chuc_vu": "Giám đốc kinh doanh"
  },
  "hang": [
    {
      "ten": "O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm",
      "so_luong": 1,
      "thanh_tien": "600 USD"
    }
  ],
  "tong_tien": "600 USD",
  "tong_tien_bang_chu": "Sáu trăm đô la Mỹ",
  "thoi_han": "12 tháng kể từ ngày ký",
  "ho_tro_trien_khai": true
}
Fill the contract template, receive a PDF
curl -sS http://localhost:8080/v1/template/render \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "template=@hop-dong-mau.docx" \
  -F "data=<-" \
  -F "to=pdf" \
  -o hop-dong.pdf -w "HTTP %{http_code}\n" <<'JSON'
{
  "so_hop_dong": "HD-2026-091",
  "ngay_ky": "21/09/2026",
  "dia_diem": "Hà Nội",
  "ben_a": {
    "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
    "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội",
    "dai_dien": "Ông Nguyễn Văn Hùng",
    "chuc_vu": "Giám đốc"
  },
  "ben_b": {
    "ten": "Công ty Cổ phần O3O",
    "dai_dien": "Bà Trần Thu Hà",
    "chuc_vu": "Giám đốc kinh doanh"
  },
  "hang": [
    {
      "ten": "O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm",
      "so_luong": 1,
      "thanh_tien": "600 USD"
    }
  ],
  "tong_tien": "600 USD",
  "tong_tien_bang_chu": "Sáu trăm đô la Mỹ",
  "thoi_han": "12 tháng kể từ ngày ký",
  "ho_tro_trien_khai": true
}
JSON
import { readFile, writeFile } from "node:fs/promises";

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

const form = new FormData();
form.append("template", new Blob([await readFile("hop-dong-mau.docx")]), "hop-dong-mau.docx");
form.append("data", JSON.stringify({
  "so_hop_dong": "HD-2026-091",
  "ngay_ky": "21/09/2026",
  "dia_diem": "Hà Nội",
  "ben_a": {
    "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
    "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội",
    "dai_dien": "Ông Nguyễn Văn Hùng",
    "chuc_vu": "Giám đốc"
  },
  "ben_b": {
    "ten": "Công ty Cổ phần O3O",
    "dai_dien": "Bà Trần Thu Hà",
    "chuc_vu": "Giám đốc kinh doanh"
  },
  "hang": [
    {
      "ten": "O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm",
      "so_luong": 1,
      "thanh_tien": "600 USD"
    }
  ],
  "tong_tien": "600 USD",
  "tong_tien_bang_chu": "Sáu trăm đô la Mỹ",
  "thoi_han": "12 tháng kể từ ngày ký",
  "ho_tro_trien_khai": true
}));
form.append("to", "pdf");

const res = await fetch(`${BASE_URL}/v1/template/render`, {
  method: "POST",
  headers: HEADERS,
  body: form,
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("hop-dong.pdf", Buffer.from(await res.arrayBuffer()));
import json

import requests

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

with open("hop-dong-mau.docx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/template/render",
        headers=HEADERS,
        files={"template": ("hop-dong-mau.docx", f)},
        data={
            "data": json.dumps({
                "so_hop_dong": "HD-2026-091",
                "ngay_ky": "21/09/2026",
                "dia_diem": "Hà Nội",
                "ben_a": {
                    "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
                    "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội",
                    "dai_dien": "Ông Nguyễn Văn Hùng",
                    "chuc_vu": "Giám đốc"
                },
                "ben_b": {
                    "ten": "Công ty Cổ phần O3O",
                    "dai_dien": "Bà Trần Thu Hà",
                    "chuc_vu": "Giám đốc kinh doanh"
                },
                "hang": [
                    {
                        "ten": "O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm",
                        "so_luong": 1,
                        "thanh_tien": "600 USD"
                    }
                ],
                "tong_tien": "600 USD",
                "tong_tien_bang_chu": "Sáu trăm đô la Mỹ",
                "thoi_han": "12 tháng kể từ ngày ký",
                "ho_tro_trien_khai": True
            }, ensure_ascii=False),
            "to": "pdf",
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("hop-dong.pdf", "wb") as fh:
    fh.write(r.content)
<?php
$ch = curl_init("http://localhost:8080/v1/template/render");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_POSTFIELDS => [
        'template' => new CURLFile('hop-dong-mau.docx'),
        'data' => json_encode([
            'so_hop_dong' => 'HD-2026-091',
            'ngay_ky' => '21/09/2026',
            'dia_diem' => 'Hà Nội',
            'ben_a' => [
                'ten' => 'Công ty TNHH Thương mại và Dịch vụ An Phát',
                'dia_chi' => 'Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội',
                'dai_dien' => 'Ông Nguyễn Văn Hùng',
                'chuc_vu' => 'Giám đốc',
            ],
            'ben_b' => [
                'ten' => 'Công ty Cổ phần O3O',
                'dai_dien' => 'Bà Trần Thu Hà',
                'chuc_vu' => 'Giám đốc kinh doanh',
            ],
            'hang' => [
                [
                    'ten' => 'O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm',
                    'so_luong' => 1,
                    'thanh_tien' => '600 USD',
                ],
            ],
            'tong_tien' => '600 USD',
            'tong_tien_bang_chu' => 'Sáu trăm đô la Mỹ',
            'thoi_han' => '12 tháng kể từ ngày ký',
            'ho_tro_trien_khai' => true,
        ], JSON_UNESCAPED_UNICODE),
        'to' => 'pdf',
    ],
    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("hop-dong.pdf", $body);
using System.Net.Http.Headers;

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

var data = """
    {
      "so_hop_dong": "HD-2026-091",
      "ngay_ky": "21/09/2026",
      "dia_diem": "Hà Nội",
      "ben_a": {
        "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
        "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội",
        "dai_dien": "Ông Nguyễn Văn Hùng",
        "chuc_vu": "Giám đốc"
      },
      "ben_b": {
        "ten": "Công ty Cổ phần O3O",
        "dai_dien": "Bà Trần Thu Hà",
        "chuc_vu": "Giám đốc kinh doanh"
      },
      "hang": [
        {
          "ten": "O3O Office Online, bản doanh nghiệp, 120 kết nối, thuê bao 1 năm",
          "so_luong": 1,
          "thanh_tien": "600 USD"
        }
      ],
      "tong_tien": "600 USD",
      "tong_tien_bang_chu": "Sáu trăm đô la Mỹ",
      "thoi_han": "12 tháng kể từ ngày ký",
      "ho_tro_trien_khai": true
    }
    """;

using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("hop-dong-mau.docx")), "template", "hop-dong-mau.docx");
form.Add(new StringContent(data), "data");
form.Add(new StringContent("pdf"), "to");

using var res = await http.PostAsync("http://localhost:8080/v1/template/render", form);
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("hop-dong.pdf", await res.Content.ReadAsByteArrayAsync());
200Synchronous mode: the response body is the binary file. The block below lists the headers that come with it (Community plan, so the per-day headers are present).
{
  "X-O3O-Request-Id": "req_0123456789abcdef",
  "X-O3O-Job-Id": "job_5f0c2a9e41b7d3c8a6e1f024",
  "X-O3O-File-Id": "file_9b3e7d21c4a8f0e65d1b2c37",
  "Content-Disposition": "attachment; filename=\"hop-dong.pdf\"",
  "X-RateLimit-Limit": "10",
  "X-RateLimit-Remaining": "9",
  "X-RateLimit-Reset": "1789984860",
  "X-RateLimit-Limit-Day": "200",
  "X-RateLimit-Remaining-Day": "187"
}

Example: sales invoice#

The template hoa-don-mau.docx has a top part Số: {{so}}, Ngày: {{ngay}}, Khách hàng: {{khach.ten}}, Địa chỉ: {{khach.dia_chi}}; a table with a loop row; and a bottom part Cộng tiền hàng: {{tong_tien}}, Ghi chú: {{ghi_chu}}, Người lập: {{nguoi_lap}}. The table in the template:

STTItemUnitQuantityUnit priceAmount
{{#hang}}{{@index}}{{ten}}{{dvt}}{{so_luong}}{{don_gia}}{{thanh_tien}}{{/hang}}
JSONData
{
  "so": "0000412",
  "ngay": "21/09/2026",
  "khach": {
    "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
    "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
  },
  "hang": [
    {
      "ten": "Giấy in A4 định lượng 70",
      "dvt": "ram",
      "so_luong": 20,
      "don_gia": "68.000",
      "thanh_tien": "1.360.000"
    },
    {
      "ten": "Mực in laser",
      "dvt": "hộp",
      "so_luong": 2,
      "don_gia": "450.000",
      "thanh_tien": "900.000"
    },
    {
      "ten": "Bìa hồ sơ",
      "dvt": "chiếc",
      "so_luong": 50,
      "don_gia": "3.500",
      "thanh_tien": "175.000"
    }
  ],
  "tong_tien": "2.435.000",
  "ghi_chu": "Thanh toán chuyển khoản trong 7 ngày.\nNội dung chuyển khoản: số hoá đơn.",
  "nguoi_lap": "Lê Minh Châu"
}
Template from a URL, fail on missing fields
curl -sS http://localhost:8080/v1/template/render \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  -o hoa-don-0000412.pdf -w "HTTP %{http_code}\n" <<'JSON'
{
  "template_url": "https://example.com/mau/hoa-don-mau.docx",
  "data": {
    "so": "0000412",
    "ngay": "21/09/2026",
    "khach": {
      "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
      "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
    },
    "hang": [
      {
        "ten": "Giấy in A4 định lượng 70",
        "dvt": "ram",
        "so_luong": 20,
        "don_gia": "68.000",
        "thanh_tien": "1.360.000"
      },
      {
        "ten": "Mực in laser",
        "dvt": "hộp",
        "so_luong": 2,
        "don_gia": "450.000",
        "thanh_tien": "900.000"
      },
      {
        "ten": "Bìa hồ sơ",
        "dvt": "chiếc",
        "so_luong": 50,
        "don_gia": "3.500",
        "thanh_tien": "175.000"
      }
    ],
    "tong_tien": "2.435.000",
    "ghi_chu": "Thanh toán chuyển khoản trong 7 ngày.\nNội dung chuyển khoản: số hoá đơn.",
    "nguoi_lap": "Lê Minh Châu"
  },
  "to": "pdf",
  "filename": "hoa-don-0000412.pdf",
  "options": {
    "missing": "error"
  }
}
JSON
import { writeFile } from "node:fs/promises";

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

const payload = {
  "template_url": "https://example.com/mau/hoa-don-mau.docx",
  "data": {
    "so": "0000412",
    "ngay": "21/09/2026",
    "khach": {
      "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
      "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
    },
    "hang": [
      {
        "ten": "Giấy in A4 định lượng 70",
        "dvt": "ram",
        "so_luong": 20,
        "don_gia": "68.000",
        "thanh_tien": "1.360.000"
      },
      {
        "ten": "Mực in laser",
        "dvt": "hộp",
        "so_luong": 2,
        "don_gia": "450.000",
        "thanh_tien": "900.000"
      },
      {
        "ten": "Bìa hồ sơ",
        "dvt": "chiếc",
        "so_luong": 50,
        "don_gia": "3.500",
        "thanh_tien": "175.000"
      }
    ],
    "tong_tien": "2.435.000",
    "ghi_chu": "Thanh toán chuyển khoản trong 7 ngày.\nNội dung chuyển khoản: số hoá đơn.",
    "nguoi_lap": "Lê Minh Châu"
  },
  "to": "pdf",
  "filename": "hoa-don-0000412.pdf",
  "options": {
    "missing": "error"
  }
};

const res = await fetch(`${BASE_URL}/v1/template/render`, {
  method: "POST",
  headers: { ...HEADERS, "Content-Type": "application/json" },
  body: JSON.stringify(payload),
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("hoa-don-0000412.pdf", Buffer.from(await res.arrayBuffer()));
import requests

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

payload = {
    "template_url": "https://example.com/mau/hoa-don-mau.docx",
    "data": {
        "so": "0000412",
        "ngay": "21/09/2026",
        "khach": {
            "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
            "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
        },
        "hang": [
            {
                "ten": "Giấy in A4 định lượng 70",
                "dvt": "ram",
                "so_luong": 20,
                "don_gia": "68.000",
                "thanh_tien": "1.360.000"
            },
            {
                "ten": "Mực in laser",
                "dvt": "hộp",
                "so_luong": 2,
                "don_gia": "450.000",
                "thanh_tien": "900.000"
            },
            {
                "ten": "Bìa hồ sơ",
                "dvt": "chiếc",
                "so_luong": 50,
                "don_gia": "3.500",
                "thanh_tien": "175.000"
            }
        ],
        "tong_tien": "2.435.000",
        "ghi_chu": "Thanh toán chuyển khoản trong 7 ngày.\nNội dung chuyển khoản: số hoá đơn.",
        "nguoi_lap": "Lê Minh Châu"
    },
    "to": "pdf",
    "filename": "hoa-don-0000412.pdf",
    "options": {
        "missing": "error"
    }
}
r = requests.post(f"{BASE_URL}/v1/template/render", headers=HEADERS, json=payload, timeout=90)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("hoa-don-0000412.pdf", "wb") as fh:
    fh.write(r.content)
<?php
$payload = json_encode([
    'template_url' => 'https://example.com/mau/hoa-don-mau.docx',
    'data' => [
        'so' => '0000412',
        'ngay' => '21/09/2026',
        'khach' => [
            'ten' => 'Công ty TNHH Thương mại và Dịch vụ An Phát',
            'dia_chi' => 'Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội',
        ],
        'hang' => [
            [
                'ten' => 'Giấy in A4 định lượng 70',
                'dvt' => 'ram',
                'so_luong' => 20,
                'don_gia' => '68.000',
                'thanh_tien' => '1.360.000',
            ],
            [
                'ten' => 'Mực in laser',
                'dvt' => 'hộp',
                'so_luong' => 2,
                'don_gia' => '450.000',
                'thanh_tien' => '900.000',
            ],
            [
                'ten' => 'Bìa hồ sơ',
                'dvt' => 'chiếc',
                'so_luong' => 50,
                'don_gia' => '3.500',
                'thanh_tien' => '175.000',
            ],
        ],
        'tong_tien' => '2.435.000',
        'ghi_chu' => 'Thanh toán chuyển khoản trong 7 ngày.
Nội dung chuyển khoản: số hoá đơn.',
        'nguoi_lap' => 'Lê Minh Châu',
    ],
    'to' => 'pdf',
    'filename' => 'hoa-don-0000412.pdf',
    'options' => ['missing' => 'error'],
], JSON_UNESCAPED_UNICODE);

$ch = curl_init("http://localhost:8080/v1/template/render");
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("hoa-don-0000412.pdf", $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 = """
    {
      "template_url": "https://example.com/mau/hoa-don-mau.docx",
      "data": {
        "so": "0000412",
        "ngay": "21/09/2026",
        "khach": {
          "ten": "Công ty TNHH Thương mại và Dịch vụ An Phát",
          "dia_chi": "Số 18 đường Trần Hưng Đạo, quận Hoàn Kiếm, Hà Nội"
        },
        "hang": [
          {
            "ten": "Giấy in A4 định lượng 70",
            "dvt": "ram",
            "so_luong": 20,
            "don_gia": "68.000",
            "thanh_tien": "1.360.000"
          },
          {
            "ten": "Mực in laser",
            "dvt": "hộp",
            "so_luong": 2,
            "don_gia": "450.000",
            "thanh_tien": "900.000"
          },
          {
            "ten": "Bìa hồ sơ",
            "dvt": "chiếc",
            "so_luong": 50,
            "don_gia": "3.500",
            "thanh_tien": "175.000"
          }
        ],
        "tong_tien": "2.435.000",
        "ghi_chu": "Thanh toán chuyển khoản trong 7 ngày.\nNội dung chuyển khoản: số hoá đơn.",
        "nguoi_lap": "Lê Minh Châu"
      },
      "to": "pdf",
      "filename": "hoa-don-0000412.pdf",
      "options": {
        "missing": "error"
      }
    }
    """;
using var res = await http.PostAsync("http://localhost:8080/v1/template/render",
    new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("hoa-don-0000412.pdf", await res.Content.ReadAsByteArrayAsync());

After filling, the table has three item rows; {{ghi_chu}} prints on two lines thanks to \n:

STTItemUnitQuantityUnit priceAmount
1Giấy in A4 định lượng 70ram2068.0001.360.000
2Mực in laserhộp2450.000900.000
3Bìa hồ sơchiếc503.500175.000

Common errors#

HTTPCodeWhen
400bad_requestMissing template or template_url, data not an object, malformed JSON.
401unauthorizedMissing or wrong key. detail.reason is missing, invalid or no_keys_configured.
403forbidden_featureCalling this endpoint on the Community plan.
413file_too_largeInput or output file exceeds max_file_mb. detail.limit_mb.
415unsupported_formatThe template is not docx or odt.
422template_errorUnbalanced loop tags, missing fields with missing = "error", or an array or object in a plain field. detail.fields.
422macro_not_allowedThe template contains macros (a vbaProject.bin part, a macroEnabled content type, or a Basic/ or Scripts/ folder).
422corrupt_sourceLibreOffice cannot open the source file.
422url_not_allowedThe URL violates the SSRF protection rules.
422download_failedThe source URL could not be downloaded.
429rate_limitedRate limit exceeded. detail.window is minute or day; comes with Retry-After.
504timeoutSynchronous mode exceeded sync_timeout_seconds, or the job exceeded job_timeout_seconds. The worker is killed and restarted.
403Called on the Community plan.
{
  "error": {
    "code": "forbidden_feature",
    "message": "Điền mẫu chỉ có ở bản doanh nghiệp.",
    "detail": {
      "feature": "POST /v1/template/render",
      "edition": "community"
    },
    "request_id": "req_0123456789abcdef"
  }
}
422Missing fields with options.missing = "error".
{
  "error": {
    "code": "template_error",
    "message": "Mẫu có trường không có trong dữ liệu.",
    "detail": {
      "fields": [
        "ben_a.dai_dien",
        "tong_tien"
      ]
    },
    "request_id": "req_0123456789abcdef"
  }
}
422The template contains macros.
{
  "error": {
    "code": "macro_not_allowed",
    "message": "Mẫu có chứa macro. DocBuilder không nhận mẫu có macro: hãy lưu lại thành docx hoặc odt không có macro rồi gửi lại.",
    "detail": {
      "reason": "vba_project",
      "part": "word/vbaProject.bin"
    },
    "request_id": "req_0123456789abcdef"
  }
}

Coming later#

Coming soon

Conditions and images in templates, date and money filters, batch filling in one call (/v1/template/render-batch) and listing a template's fields (/v1/template/inspect).