O3O DocBuilder và chuyển đổi
Điền mẫu
POST /v1/template/render điền dữ liệu JSON vào mẫu docx hoặc odt do người dùng tự soạn: trường {{ten}}, đường dẫn lồng nhau, lặp hàng bảng; trả docx, odt hoặc PDF. Chỉ có ở bản doanh nghiệp.
Trong trang này
Bộ phận nghiệp vụ soạn mẫu bằng trình soạn thảo quen thuộc, đặt các thẻ {{ten_truong}} vào chỗ cần điền; hệ thống của bạn chỉ gửi dữ liệu JSON. Đổi câu chữ hay bố cục của mẫu không cần sửa mã.
/v1/template/renderĐiền dữ liệu JSON vào mẫu docx hoặc odt.
Soạn mẫu#
Mẫu là tệp docx hoặc odt soạn bình thường. Chỗ cần điền gõ thẳng dưới dạng chữ trong cặp ngoặc nhọn kép. Thay thế áp cho thân văn bản, bảng, đầu trang và chân trang; định dạng chữ tại vị trí đặt thẻ (font, cỡ, đậm, màu) được giữ nguyên.
| Cú pháp | Ý nghĩa |
|---|---|
{{ten}} | Thay bằng data.ten. Tên trường khớp [A-Za-z_][A-Za-z0-9_]*; cho phép khoảng trắng sát hai dấu ngoặc, ví dụ {{ ten }}. |
{{khach.ten}} | Đi theo đường dẫn vào đối tượng lồng nhau. |
{{#hang}} … {{/hang}} | Lặp hàng bảng theo mảng data.hang, xem mục dưới. |
{{ten}} trong hàng lặp | Tra trong phần tử hiện tại trước, không có thì tra ở gốc data. |
{{@index}} | Số thứ tự trong hàng lặp, bắt đầu từ 1. |
Giá trị được in thế nào#
| Giá trị trong data | In ra |
|---|---|
| Chuỗi | Giữ nguyên. Ký tự \n thành ngắt dòng trong đoạn. |
| Số nguyên | Không có phần thập phân: 20. |
| Số thực | Tối đa 6 chữ số có nghĩa. |
true, false | Có, Không. |
null | Chuỗi rỗng. |
| Mảng hoặc đối tượng ở vị trí trường đơn | Lỗi 422 template_error. |
Lặp hàng bảng#
Đặt {{#hang}} ở ô ĐẦU và {{/hang}} ở ô CUỐI của CÙNG một hàng bảng. Hàng đó được nhân bản một lần cho mỗi phần tử của mảng data.hang, hai thẻ bị xoá; mảng rỗng thì hàng mẫu bị xoá. Hàng tiêu đề và các hàng khác của bảng giữ nguyên.
| STT | Tên hàng hoá, dịch vụ | Số lượng | Thành tiền |
|---|---|---|---|
{{#hang}}{{@index}} | {{ten}} | {{so_luong}} | {{thanh_tien}}{{/hang}} |
Trường thiếu#
options.missing | Trường không có trong data |
|---|---|
empty | Mặc định. Thay bằng chuỗi rỗng. |
keep | Để nguyên thẻ trong tài liệu, tiện khi soát mẫu. |
error | Trả 422 template_error, detail.fields liệt kê trường thiếu. |
Mẫu có macro bị từ chối#
Điền mẫu đọc và ghi thẳng các phần XML của tệp mẫu, không nạp mẫu qua LibreOffice; các phần khác của tệp được chép nguyên sang tệp kết quả, nên nếu nhận mẫu có macro thì macro sẽ nằm nguyên trong tệp kết quả. Vì vậy DocBuilder từ chối mọi mẫu có macro, kể cả tệp docm đã đổi đuôi thành docx, và trả 422 macro_not_allowed kèm thông điệp tiếng Việt. Mẫu bị coi là có macro khi:
- Tệp OOXML (docx) có phần
vbaProject.bin, khai kiểu nội dungmacroEnabledtrong[Content_Types].xml, hoặc có quan hệ trỏ tới phần VBA (kể cả khi phần đó bị đổi tên). - Tệp ODF (odt) có thư mục
Basic/hoặcScripts/.
Mẫu được kiểm ngay khi nhận, trước khi tạo job, nên lỗi trả về ngay cả khi async = true. detail.reason cho biết lý do (vba_project, macro_enabled_content_type, odf_basic, odf_scripts); detail.part là tên phần gây lỗi trong tệp.
Tham số#
Thân multipart/form-data
templatefilebắt buộcTệp mẫu docx hoặc odt.datastring (JSON)bắt buộcDữ liệu, gửi dạng CHUỖI JSON; gốc phải là đối tượng.todocx | odt | pdftuỳ chọnMặc định:định dạng của mẫuĐịnh dạng đích.filenamestring, ≤ 255tuỳ chọnTên tệp kết quả.optionsstring (JSON)tuỳ chọnVí dụ{"missing": "error"}.asyncbooleantuỳ chọnMặc định:falsetrue: trả ngay202kèm job.callback_urlstring (URL)tuỳ chọnCần tính năngcallbacktrong token.
Thân application/json
template_urlURL, ≤ 2048bắt buộcURL tải mẫu, chịu quy tắc chống SSRF.dataobjectbắt buộcDữ liệu điền vào mẫu.todocx | odt | pdftuỳ chọnMặc định:định dạng của mẫuĐịnh dạng đích.filenamestring, ≤ 255tuỳ chọnTên tệp kết quả.optionsobjecttuỳ chọnTuỳ chọn điền mẫu.asyncbooleantuỳ chọnMặc định:falsetrue: trả ngay202kèm job.callback_urlstring (URL)tuỳ chọnCần tính năngcallbacktrong token.
Đối tượng options
missingempty | keep | errortuỳ chọnMặc định:emptyCách xử lý trường thiếu.
Ví dụ: hợp đồng dịch vụ#
Mẫu hop-dong-mau.docx gồm các đoạn dưới đây (cột trái là chữ gõ trong mẫu, cột phải là kết quả sau khi điền dữ liệu bên dưới).
| Gõ trong mẫu | Kết quả |
|---|---|
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ó. |
{
"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
}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
}
JSONimport { 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());{
"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"
}Ví dụ: hoá đơn bán hàng#
Mẫu hoa-don-mau.docx có phần đầu Số: {{so}}, Ngày: {{ngay}}, Khách hàng: {{khach.ten}}, Địa chỉ: {{khach.dia_chi}}; một bảng có hàng lặp; và phần cuối Cộng tiền hàng: {{tong_tien}}, Ghi chú: {{ghi_chu}}, Người lập: {{nguoi_lap}}. Bảng trong mẫu:
| STT | Tên hàng hoá, dịch vụ | Đơn vị tính | Số lượng | Đơn giá | Thành tiền |
|---|---|---|---|---|---|
{{#hang}}{{@index}} | {{ten}} | {{dvt}} | {{so_luong}} | {{don_gia}} | {{thanh_tien}}{{/hang}} |
{
"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"
}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"
}
}
JSONimport { 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());Bảng sau khi điền có ba hàng hàng hoá; {{ghi_chu}} in thành hai dòng nhờ ký tự \n:
| STT | Tên hàng hoá, dịch vụ | Đơn vị tính | Số lượng | Đơn giá | Thành tiền |
|---|---|---|---|---|---|
| 1 | Giấy in A4 định lượng 70 | ram | 20 | 68.000 | 1.360.000 |
| 2 | Mực in laser | hộp | 2 | 450.000 | 900.000 |
| 3 | Bìa hồ sơ | chiếc | 50 | 3.500 | 175.000 |
Mã lỗi thường gặp#
| HTTP | Mã | Khi nào |
|---|---|---|
| 400 | bad_request | Thiếu template hoặc template_url, data không phải đối tượng, JSON hỏng. |
| 401 | unauthorized | Thiếu hoặc sai khoá. detail.reason là missing, invalid hoặc no_keys_configured. |
| 403 | forbidden_feature | Gọi endpoint này ở bản cộng đồng. |
| 413 | file_too_large | Tệp vào hoặc tệp ra vượt max_file_mb. detail.limit_mb. |
| 415 | unsupported_format | Mẫu không phải docx hoặc odt. |
| 422 | template_error | Thẻ lặp không cân, trường thiếu khi missing = "error", hoặc mảng, đối tượng ở vị trí trường đơn. detail.fields. |
| 422 | macro_not_allowed | Mẫu có macro (phần vbaProject.bin, kiểu nội dung macroEnabled, thư mục Basic/ hay Scripts/). |
| 422 | corrupt_source | LibreOffice không mở được tệp nguồn. |
| 422 | url_not_allowed | URL vi phạm quy tắc chống SSRF. |
| 422 | download_failed | Không tải được URL nguồn. |
| 429 | rate_limited | Vượt hạn mức. detail.window là minute hoặc day; kèm Retry-After. |
| 504 | timeout | Chế độ đồng bộ quá sync_timeout_seconds, hoặc job quá job_timeout_seconds. Worker bị dừng và dựng lại. |
{
"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"
}
}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"
}
}{
"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"
}
}Sắp có#
Điều kiện và ảnh trong mẫu, bộ lọc ngày và tiền, điền mẫu hàng loạt trong một lần gọi (/v1/template/render-batch) và soi danh sách trường của mẫu (/v1/template/inspect).