Skip to content

O3O DocBuilder and conversion

Format conversion

POST /v1/convert turns a file into another format of the same family (text, spreadsheet, presentation), including PDF, PDF/A and a PNG or JPG of one page; synchronous or asynchronous, from an upload or a URL.

On this page

Send a source file, get the target file back. The source arrives in one of two ways: an upload with multipart/form-data (field file), or application/json with a url that DocBuilder downloads itself. Conversion stays within one document family; crossing families, for example docx to xlsx, returns 415 unsupported_format.

POST/v1/convert

Convert a file. Synchronous mode returns the file; asynchronous mode returns a job.

Auth: BearerCommunityEnterprise

Parameters#

multipart/form-data body

  • filefilerequired
    Source file.
  • tostringrequired
    Target format: docx, odt, rtf, txt, html, pdf, xlsx, ods, csv, pptx, odp, png, jpg.
  • fromstringoptional
    Source format: docx, doc, odt, rtf, txt, html, xlsx, xls, ods, csv, pptx, ppt, odp. When absent, the file name extension is used.
  • filenamestringoptionalDefault: source name with the new extension
    Result file name, up to 255 characters.
  • optionsstring (JSON)optional
    Conversion options, sent as a JSON STRING (see the table below).
  • asyncbooleanoptionalDefault: false
    true: return 202 with a job right away instead of waiting for the file.
  • callback_urlstring (URL)optional
    Enterprise with the callback feature only. Receives a POST when the job ends.

application/json body

  • urlstring (URL)required
    URL of the source file, up to 2048 characters, subject to the SSRF rules.
  • tostringrequired
    Target format: docx, odt, rtf, txt, html, pdf, xlsx, ods, csv, pptx, odp, png, jpg.
  • fromstringoptional
    Source format: docx, doc, odt, rtf, txt, html, xlsx, xls, ods, csv, pptx, ppt, odp. When absent, the file name extension is used.
  • filenamestringoptionalDefault: source name with the new extension
    Result file name, up to 255 characters.
  • optionsobjectoptional
    Conversion options (see the table below).
  • asyncbooleanoptionalDefault: false
    true: return 202 with a job right away instead of waiting for the file.
  • callback_urlstring (URL)optional
    Enterprise with the callback feature only. Receives a POST when the job ends.

The options object

  • passwordstringoptional
    Password to OPEN the source file. Missing or wrong: 422 password_required.
  • pdf.pdfabooleanoptionalDefault: false
    Export PDF/A-2b for long-term archiving.
  • pdf.page_rangestringoptionalDefault: all pages
    Page range such as 1-3,5. Pattern: ^[0-9]+(-[0-9]+)?(,[0-9]+(-[0-9]+)?)*$.
  • pdf.image_qualityinteger 1–100optionalDefault: 90
    Quality of images inside the PDF.
  • image.pageinteger ≥ 1optionalDefault: 1
    Page rendered when the target is png or jpg.
  • image.widthinteger 16–4000optionalDefault: 1240
    Image width in pixels; the height follows the page ratio.
  • csv.delimiterstring, 1 characteroptionalDefault: ,
    Column separator.
  • csv.encodingutf-8 | windows-1258 | windows-1252optionalDefault: utf-8
    CSV character encoding.
  • csv.sheetinteger ≥ 1optionalDefault: 1
    Sheet exported when the target is csv.

Synchronous conversion#

By default (async = false) DocBuilder holds the connection until the work is done and returns the result file with status 200. Beyond sync_timeout_seconds (60 seconds on both plans) it returns 504 timeout; use asynchronous mode for large or long files. The file ID in the X-O3O-File-Id header can be downloaded again from GET /v1/files/{id} until it expires.

Convert docx to PDF
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@bao-cao.docx" \
  -F "to=pdf" \
  -o bao-cao.pdf -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 form = new FormData();
form.append("file", new Blob([await readFile("bao-cao.docx")]), "bao-cao.docx");
form.append("to", "pdf");

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

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

with open("bao-cao.docx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("bao-cao.docx", f)},
        data={
            "to": "pdf",
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bao-cao.pdf", "wb") as fh:
    fh.write(r.content)
<?php
$ch = curl_init("http://localhost:8080/v1/convert");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('bao-cao.docx'),
        '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("bao-cao.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");

using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("bao-cao.docx")), "file", "bao-cao.docx");
form.Add(new StringContent("pdf"), "to");

using var res = await http.PostAsync("http://localhost:8080/v1/convert", form);
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("bao-cao.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=\"bao-cao.pdf\"",
  "X-RateLimit-Limit": "10",
  "X-RateLimit-Remaining": "9",
  "X-RateLimit-Reset": "1789984860",
  "X-RateLimit-Limit-Day": "200",
  "X-RateLimit-Remaining-Day": "187"
}

Source from a URL#

Send application/json with url. DocBuilder downloads the file under the SSRF rules: http and https only, no user:pass@ part, at most 2048 characters; every address the host name resolves to must be public, and DocBuilder connects straight to the checked IP address without resolving again (DNS rebinding protection); up to 3 redirects, each hop checked again; only status 200 counts as success; the download time limit is O3O_FETCH_TIMEOUT_SECONDS (30 seconds by default).

Convert a file from a URL to PDF/A
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/files/bao-cao.docx", "to": "pdf", "options": {"pdf": {"pdfa": true}}}' \
  -o bao-cao.pdf -w "HTTP %{http_code}\n"
import { writeFile } from "node:fs/promises";

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

const payload = {
  "url": "https://example.com/files/bao-cao.docx",
  "to": "pdf",
  "options": {
    "pdf": {
      "pdfa": true
    }
  }
};

const res = await fetch(`${BASE_URL}/v1/convert`, {
  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("bao-cao.pdf", Buffer.from(await res.arrayBuffer()));
import requests

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

payload = {
    "url": "https://example.com/files/bao-cao.docx",
    "to": "pdf",
    "options": {
        "pdf": {
            "pdfa": True
        }
    }
}
r = requests.post(f"{BASE_URL}/v1/convert", headers=HEADERS, json=payload, timeout=90)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bao-cao.pdf", "wb") as fh:
    fh.write(r.content)
<?php
$payload = json_encode([
    'url' => 'https://example.com/files/bao-cao.docx',
    'to' => 'pdf',
    'options' => ['pdf' => ['pdfa' => true]],
], JSON_UNESCAPED_UNICODE);

$ch = curl_init("http://localhost:8080/v1/convert");
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-cao.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 = """
    {
      "url": "https://example.com/files/bao-cao.docx",
      "to": "pdf",
      "options": {
        "pdf": {
          "pdfa": true
        }
      }
    }
    """;
using var res = await http.PostAsync("http://localhost:8080/v1/convert",
    new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("bao-cao.pdf", await res.Content.ReadAsByteArrayAsync());

Asynchronous conversion#

Set async = true to get 202 right away with a job object and the header Location: /v1/jobs/{id}. Then poll and download as described in Async jobs. Both plans can use this mode; callback_url, however, needs Enterprise with the callback feature, and Community receives 403 forbidden_feature for it.

Submit a conversion job
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@bao-cao.docx" \
  -F "to=pdf" \
  -F "async=true"
import { readFile } from "node:fs/promises";

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

const form = new FormData();
form.append("file", new Blob([await readFile("bao-cao.docx")]), "bao-cao.docx");
form.append("to", "pdf");
form.append("async", "true");

const res = await fetch(`${BASE_URL}/v1/convert`, {
  method: "POST",
  headers: HEADERS,
  body: form,
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());
import requests

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

with open("bao-cao.docx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("bao-cao.docx", f)},
        data={
            "to": "pdf",
            "async": "true",
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())
<?php
$ch = curl_init("http://localhost:8080/v1/convert");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('bao-cao.docx'),
        'to' => 'pdf',
        'async' => 'true',
    ],
    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));
}
print_r(json_decode($body, true));
using System.Net.Http.Headers;

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

using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync("bao-cao.docx")), "file", "bao-cao.docx");
form.Add(new StringContent("pdf"), "to");
form.Add(new StringContent("true"), "async");

using var res = await http.PostAsync("http://localhost:8080/v1/convert", form);
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());
202The job is queued. The Location header points to /v1/jobs/{id}.
{
  "id": "job_5f0c2a9e41b7d3c8a6e1f024",
  "kind": "convert",
  "status": "queued",
  "created_at": "2026-09-21T10:00:00Z",
  "started_at": null,
  "finished_at": null,
  "duration_ms": null,
  "expires_at": null,
  "outputs": [],
  "error": null,
  "callback": null
}

PDF options#

  • pdf.pdfa = true exports PDF/A-2b, suited to long-term records.
  • pdf.page_range selects pages, for example 1-3,5 means pages 1 to 3 and page 5.
  • pdf.image_quality from 1 to 100 (default 90): lower values give smaller files and softer images.
Pages 1 to 3 and 5, PDF/A, image quality 80
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@bao-cao.docx" \
  -F "to=pdf" \
  --form-string 'options={"pdf":{"pdfa":true,"page_range":"1-3,5","image_quality":80}}' \
  -o bao-cao-trich.pdf -w "HTTP %{http_code}\n"
import json

import requests

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

with open("bao-cao.docx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("bao-cao.docx", f)},
        data={
            "to": "pdf",
            "options": json.dumps({"pdf": {"pdfa": True, "page_range": "1-3,5", "image_quality": 80}}, ensure_ascii=False),
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("bao-cao-trich.pdf", "wb") as fh:
    fh.write(r.content)
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("file", new Blob([await readFile("bao-cao.docx")]), "bao-cao.docx");
form.append("to", "pdf");
form.append("options", JSON.stringify({"pdf": {"pdfa": true, "page_range": "1-3,5", "image_quality": 80}}));

const res = await fetch(`${BASE_URL}/v1/convert`, {
  method: "POST",
  headers: HEADERS,
  body: form,
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("bao-cao-trich.pdf", Buffer.from(await res.arrayBuffer()));
Coming soon

Password-protected PDF output. Today options.password only opens a password-protected source file.

Rendering one page as an image#

A png or jpg target renders exactly one page per call, chosen with options.image.page (page 1 by default), options.image.width pixels wide (16 to 4000, default 1240), with the height following the page ratio. The image goes through an intermediate PDF and pdftoppm, so any page of a text document, spreadsheet or presentation can be chosen. For thumbnails, use POST /v1/extract/thumbnail.

Slide 2 as a 1600-pixel-wide PNG
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@gioi-thieu.pptx" \
  -F "to=png" \
  --form-string 'options={"image":{"page":2,"width":1600}}' \
  -o trang-2.png -w "HTTP %{http_code}\n"
import json

import requests

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

with open("gioi-thieu.pptx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("gioi-thieu.pptx", f)},
        data={
            "to": "png",
            "options": json.dumps({"image": {"page": 2, "width": 1600}}, ensure_ascii=False),
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("trang-2.png", "wb") as fh:
    fh.write(r.content)
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("file", new Blob([await readFile("gioi-thieu.pptx")]), "gioi-thieu.pptx");
form.append("to", "png");
form.append("options", JSON.stringify({"image": {"page": 2, "width": 1600}}));

const res = await fetch(`${BASE_URL}/v1/convert`, {
  method: "POST",
  headers: HEADERS,
  body: form,
  signal: AbortSignal.timeout(90_000),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
await writeFile("trang-2.png", Buffer.from(await res.arrayBuffer()));

CSV#

The options.csv options apply when the source or the target is csv: delimiter is one character (comma by default), encoding is utf-8, windows-1258 or windows-1252, and sheet picks the sheet exported to csv (sheet 1 by default). CSV files saved by Excel on a machine set to the Vietnamese region may use semicolons and the windows-1258 encoding; set both options to match.

Sheet 2 to a semicolon-separated CSV
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@bang-luong.xlsx" \
  -F "to=csv" \
  --form-string 'options={"csv":{"delimiter":";","sheet":2}}' \
  -o theo-phong-ban.csv -w "HTTP %{http_code}\n"
import json

import requests

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

with open("bang-luong.xlsx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("bang-luong.xlsx", f)},
        data={
            "to": "csv",
            "options": json.dumps({"csv": {"delimiter": ";", "sheet": 2}}, ensure_ascii=False),
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("theo-phong-ban.csv", "wb") as fh:
    fh.write(r.content)
Windows-1258 CSV to xlsx
curl -sS http://localhost:8080/v1/convert \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@du-lieu.csv" \
  -F "to=xlsx" \
  --form-string 'options={"csv":{"delimiter":";","encoding":"windows-1258"}}' \
  -o du-lieu.xlsx -w "HTTP %{http_code}\n"
import json

import requests

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

with open("du-lieu.csv", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/convert",
        headers=HEADERS,
        files={"file": ("du-lieu.csv", f)},
        data={
            "to": "xlsx",
            "options": json.dumps({"csv": {"delimiter": ";", "encoding": "windows-1258"}}, ensure_ascii=False),
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("du-lieu.xlsx", "wb") as fh:
    fh.write(r.content)

Supported formats#

FamilySourceTarget
textdocx, doc, odt, rtf, txt, htmldocx, odt, rtf, txt, html, pdf, png, jpg
sheetxlsx, xls, ods, csvxlsx, ods, csv, html, pdf, png, jpg
slidepptx, ppt, odppptx, odp, pdf, png, jpg

The full matrix, how DocBuilder detects the source format and the list of installed fonts: see Format matrix.

Plans and limits#

Both plans can call this endpoint. The relevant limits:

ItemCommunityEnterprise
Maximum input or output file size (max_file_mb)10 MB300 MB
Synchronous mode time limit (sync_timeout_seconds)60 seconds60 seconds
Job run time limit (job_timeout_seconds)120 seconds600 seconds
Asynchronous mode (async)yesyes
Job completion callback (callback)noyes, when the token has the callback feature

Common errors#

HTTPCodeWhen
400bad_requestMissing parameter, malformed JSON, wrong options type, or neither file nor url. detail.errors = [{path, message}].
401unauthorizedMissing or wrong key. detail.reason is missing, invalid or no_keys_configured.
403forbidden_featureSending callback_url on the Community plan.
413file_too_largeInput or output file exceeds max_file_mb. detail.limit_mb.
415unsupported_formatUnknown source format, or a cross-family conversion (for example docx to xlsx).
422corrupt_sourceLibreOffice cannot open the source file.
422password_requiredThe file is password protected and no password was sent, or the password is wrong.
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.
503queue_fullThe queue is full (max_queued_jobs); comes with Retry-After.
504timeoutSynchronous mode exceeded sync_timeout_seconds, or the job exceeded job_timeout_seconds. The worker is killed and restarted.
422The source file is password protected and options.password was not sent.
{
  "error": {
    "code": "password_required",
    "message": "Tệp có mật khẩu. Hãy gửi mật khẩu mở tệp trong options.password.",
    "detail": {},
    "request_id": "req_0123456789abcdef"
  }
}
429Per-minute limit exceeded; wait for the number of seconds in Retry-After.
{
  "error": {
    "code": "rate_limited",
    "message": "Đã vượt hạn mức yêu cầu trong phút này.",
    "detail": {
      "window": "minute"
    },
    "request_id": "req_0123456789abcdef"
  }
}