Skip to content

O3O DocBuilder and conversion

Calling the API from your code Beta

Official SDKs are in preparation; meanwhile this page gives short wrappers for curl, JavaScript, Python, PHP and C# that you can copy into a project to call every DocBuilder v1 endpoint.

This page describes a beta feature: it works but may still change.

On this page
Coming soon

Official SDKs for npm, PyPI, NuGet and Go. They will be listed on this page when released.

Meanwhile, each wrapper below fits in a single file with no dependencies beyond those noted at the top: Python needs requests; JavaScript needs Node.js 18 or later; PHP needs 8.1 or later and the cURL extension; C# needs .NET 8; the bash version needs curl 7.76 or later. The wrappers only call the endpoints of the v1 specification and raise errors carrying the server's code, detail and request_id.

Wrappers#

o3o_docbuilder.py · docbuilder.mjs · DocBuilder.php · DocBuilder.cs · o3o-docbuilder.sh
"""o3o_docbuilder.py: minimal wrapper for the O3O DocBuilder API v1; needs only the requests package."""
import json
import os
import time

import requests


class DocBuilderError(Exception):
    def __init__(self, status: int, error: dict, retry_after=None):
        super().__init__(f"{status} {error.get('code')}: {error.get('message')}")
        self.status = status
        self.code = error.get("code")
        self.detail = error.get("detail") or {}
        self.request_id = error.get("request_id")
        self.retry_after = int(retry_after) if retry_after else None


class DocBuilder:
    def __init__(self, base_url="http://localhost:8080", api_key="O3O_DEMO_KEY", timeout=90):
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"

    def _send(self, method, path, **kwargs):
        r = self.session.request(method, self.base_url + path, timeout=self.timeout, **kwargs)
        if r.status_code >= 400:
            try:
                error = r.json()["error"]
            except (ValueError, KeyError):
                error = {"code": "http_error", "message": r.text[:200]}
            raise DocBuilderError(r.status_code, error, r.headers.get("Retry-After"))
        return r

    @staticmethod
    def _result(r):
        return r.json() if r.status_code == 202 else r.content

    def _upload(self, path, field, endpoint, fields):
        data = {}
        for key, value in fields.items():
            if value is None:
                continue
            if isinstance(value, (dict, list)):
                data[key] = json.dumps(value, ensure_ascii=False)
            elif isinstance(value, bool):
                data[key] = "true" if value else "false"
            else:
                data[key] = str(value)
        with open(path, "rb") as fh:
            return self._send("POST", endpoint, files={field: (os.path.basename(path), fh)}, data=data)

    def limits(self):
        return self._send("GET", "/v1/limits").json()

    def formats(self):
        return self._send("GET", "/v1/formats").json()

    def convert(self, path, to, options=None, async_job=False):
        """Returns the result bytes, or a job dict when async_job=True."""
        fields = {"to": to, "options": options, "async": async_job}
        return self._result(self._upload(path, "file", "/v1/convert", fields))

    def convert_url(self, url, to, options=None, async_job=False):
        body = {"url": url, "to": to, "async": async_job}
        if options:
            body["options"] = options
        return self._result(self._send("POST", "/v1/convert", json=body))

    def build(self, script, async_job=False):
        return self._result(self._send("POST", "/v1/build", json={"script": script, "async": async_job}))

    def render_template(self, path, data, to=None, options=None, async_job=False):
        """Enterprise only."""
        fields = {"data": data, "to": to, "options": options, "async": async_job}
        return self._result(self._upload(path, "template", "/v1/template/render", fields))

    def extract_text(self, path, max_chars=None):
        return self._upload(path, "file", "/v1/extract/text", {"max_chars": max_chars}).json()

    def extract_meta(self, path):
        return self._upload(path, "file", "/v1/extract/meta", {}).json()

    def thumbnail(self, path, page=1, width=320, fmt="png"):
        fields = {"page": page, "width": width, "format": fmt}
        return self._upload(path, "file", "/v1/extract/thumbnail", fields).content

    def job(self, job_id):
        return self._send("GET", f"/v1/jobs/{job_id}").json()

    def wait(self, job_id, timeout=600):
        deadline, delay = time.monotonic() + timeout, 1.0
        while True:
            job = self.job(job_id)
            if job["status"] == "done":
                return job
            if job["status"] == "failed":
                raise DocBuilderError(0, job["error"])  # 0 = the job failed
            if time.monotonic() > deadline:
                raise TimeoutError(job_id)
            time.sleep(delay)
            delay = min(delay * 1.5, 5.0)

    def download(self, file_id):
        return self._send("GET", f"/v1/files/{file_id}").content
// docbuilder.mjs: minimal wrapper for the O3O DocBuilder API v1 (Node.js 18+, no dependencies).
import { readFile } from "node:fs/promises";
import { basename } from "node:path";
import { setTimeout as sleep } from "node:timers/promises";

export class DocBuilderError extends Error {
  constructor(status, error, retryAfter = null) {
    super(`${status} ${error.code}: ${error.message}`);
    this.status = status;
    this.code = error.code;
    this.detail = error.detail ?? {};
    this.requestId = error.request_id;
    this.retryAfter = retryAfter ? Number(retryAfter) : null;
  }
}

export class DocBuilder {
  constructor(baseUrl = "http://localhost:8080", apiKey = "O3O_DEMO_KEY", timeoutMs = 90_000) {
    this.baseUrl = baseUrl.replace(/\/+$/, "");
    this.apiKey = apiKey;
    this.timeoutMs = timeoutMs;
  }

  async #send(method, path, body, headers = {}) {
    const res = await fetch(this.baseUrl + path, {
      method,
      body,
      headers: { Authorization: `Bearer ${this.apiKey}`, ...headers },
      signal: AbortSignal.timeout(this.timeoutMs),
    });
    if (res.status >= 400) {
      let error;
      try {
        error = (await res.json()).error;
      } catch {
        error = { code: "http_error", message: res.statusText };
      }
      throw new DocBuilderError(res.status, error, res.headers.get("Retry-After"));
    }
    return res;
  }

  #json(path, payload) {
    return this.#send("POST", path, JSON.stringify(payload), { "Content-Type": "application/json" });
  }

  async #upload(path, field, endpoint, fields) {
    const form = new FormData();
    form.append(field, new Blob([await readFile(path)]), basename(path));
    for (const [key, value] of Object.entries(fields)) {
      if (value === undefined || value === null) continue;
      form.append(key, typeof value === "object" ? JSON.stringify(value) : String(value));
    }
    return this.#send("POST", endpoint, form);
  }

  static async #result(res) {
    return res.status === 202 ? res.json() : Buffer.from(await res.arrayBuffer());
  }

  async limits() {
    return (await this.#send("GET", "/v1/limits")).json();
  }

  async formats() {
    return (await this.#send("GET", "/v1/formats")).json();
  }

  async convert(path, to, { options, asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#upload(path, "file", "/v1/convert", { to, options, async: asyncJob }));
  }

  async convertUrl(url, to, { options, asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#json("/v1/convert", { url, to, options, async: asyncJob }));
  }

  async build(script, { asyncJob = false } = {}) {
    return DocBuilder.#result(await this.#json("/v1/build", { script, async: asyncJob }));
  }

  // Enterprise only.
  async renderTemplate(path, data, { to, options, asyncJob = false } = {}) {
    const fields = { data, to, options, async: asyncJob };
    return DocBuilder.#result(await this.#upload(path, "template", "/v1/template/render", fields));
  }

  async extractText(path, { maxChars } = {}) {
    return (await this.#upload(path, "file", "/v1/extract/text", { max_chars: maxChars })).json();
  }

  async extractMeta(path) {
    return (await this.#upload(path, "file", "/v1/extract/meta", {})).json();
  }

  async thumbnail(path, { page = 1, width = 320, format = "png" } = {}) {
    const res = await this.#upload(path, "file", "/v1/extract/thumbnail", { page, width, format });
    return Buffer.from(await res.arrayBuffer());
  }

  async job(jobId) {
    return (await this.#send("GET", `/v1/jobs/${jobId}`)).json();
  }

  async wait(jobId, { timeoutMs = 600_000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    for (let delay = 1000; ; delay = Math.min(delay * 1.5, 5000)) {
      const job = await this.job(jobId);
      if (job.status === "done") return job;
      if (job.status === "failed") throw new DocBuilderError(0, job.error); // 0 = the job failed
      if (Date.now() > deadline) throw new Error(`Timed out waiting for job ${jobId}`);
      await sleep(delay);
    }
  }

  async download(fileId) {
    const res = await this.#send("GET", `/v1/files/${fileId}`);
    return Buffer.from(await res.arrayBuffer());
  }
}
<?php
// DocBuilder.php: minimal wrapper for the O3O DocBuilder API v1 (PHP 8.1+, cURL extension).

final class DocBuilderException extends RuntimeException
{
    public function __construct(
        public readonly int $status,
        public readonly string $errorCode,
        string $message,
        public readonly array $detail = [],
        public readonly ?string $requestId = null,
        public readonly ?int $retryAfter = null,
    ) {
        parent::__construct("$status $errorCode: $message");
    }
}

final class DocBuilder
{
    public function __construct(
        private string $baseUrl = 'http://localhost:8080',
        private string $apiKey = 'O3O_DEMO_KEY',
        private int $timeout = 90,
    ) {
        $this->baseUrl = rtrim($baseUrl, '/');
    }

    public function limits(): array
    {
        return json_decode($this->send('GET', '/v1/limits')[1], true);
    }

    public function formats(): array
    {
        return json_decode($this->send('GET', '/v1/formats')[1], true);
    }

    /** Returns the file contents, or a job array when $async = true. */
    public function convert(string $path, string $to, array $options = [], bool $async = false): string|array
    {
        $fields = ['to' => $to, 'options' => $options ?: null, 'async' => $async];
        return $this->result($this->upload($path, 'file', '/v1/convert', $fields));
    }

    public function convertUrl(string $url, string $to, array $options = [], bool $async = false): string|array
    {
        $body = ['url' => $url, 'to' => $to, 'async' => $async] + ($options ? ['options' => $options] : []);
        return $this->result($this->sendJson('/v1/convert', $body));
    }

    public function build(array $script, bool $async = false): string|array
    {
        return $this->result($this->sendJson('/v1/build', ['script' => $script, 'async' => $async]));
    }

    /** Enterprise only. */
    public function renderTemplate(string $path, array $data, ?string $to = null, array $options = [], bool $async = false): string|array
    {
        $fields = ['data' => $data, 'to' => $to, 'options' => $options ?: null, 'async' => $async];
        return $this->result($this->upload($path, 'template', '/v1/template/render', $fields));
    }

    public function extractText(string $path, ?int $maxChars = null): array
    {
        return json_decode($this->upload($path, 'file', '/v1/extract/text', ['max_chars' => $maxChars])[1], true);
    }

    public function extractMeta(string $path): array
    {
        return json_decode($this->upload($path, 'file', '/v1/extract/meta', [])[1], true);
    }

    public function thumbnail(string $path, int $page = 1, int $width = 320, string $format = 'png'): string
    {
        $fields = ['page' => $page, 'width' => $width, 'format' => $format];
        return $this->upload($path, 'file', '/v1/extract/thumbnail', $fields)[1];
    }

    public function job(string $jobId): array
    {
        return json_decode($this->send('GET', "/v1/jobs/$jobId")[1], true);
    }

    public function wait(string $jobId, int $timeout = 600): array
    {
        $deadline = time() + $timeout;
        for ($delay = 1.0; ; $delay = min($delay * 1.5, 5.0)) {
            $job = $this->job($jobId);
            if ($job['status'] === 'done') {
                return $job;
            }
            if ($job['status'] === 'failed') {
                $e = $job['error'];
                throw new DocBuilderException(0, $e['code'], $e['message'], $e['detail'] ?? [], $e['request_id'] ?? null);
            }
            if (time() > $deadline) {
                throw new RuntimeException("Timed out waiting for job $jobId");
            }
            usleep((int) ($delay * 1_000_000));
        }
    }

    public function download(string $fileId): string
    {
        return $this->send('GET', "/v1/files/$fileId")[1];
    }

    private function upload(string $path, string $field, string $endpoint, array $fields): array
    {
        $post = [$field => new CURLFile($path, '', basename($path))];
        foreach ($fields as $key => $value) {
            if ($value === null) {
                continue;
            }
            if (is_array($value)) {
                $post[$key] = json_encode($value, JSON_UNESCAPED_UNICODE);
            } elseif (is_bool($value)) {
                $post[$key] = $value ? 'true' : 'false';
            } else {
                $post[$key] = (string) $value;
            }
        }
        return $this->send('POST', $endpoint, $post);
    }

    private function sendJson(string $endpoint, array $payload): array
    {
        $body = json_encode($payload, JSON_UNESCAPED_UNICODE);
        return $this->send('POST', $endpoint, $body, ['Content-Type: application/json']);
    }

    private function result(array $response): string|array
    {
        [$status, $body] = $response;
        return $status === 202 ? json_decode($body, true) : $body;
    }

    private function send(string $method, string $path, string|array|null $body = null, array $headers = []): array
    {
        $responseHeaders = [];
        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_HTTPHEADER => array_merge(['Authorization: Bearer ' . $this->apiKey], $headers),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT => $this->timeout,
            CURLOPT_HEADERFUNCTION => function ($ch, string $line) use (&$responseHeaders): int {
                $parts = explode(':', $line, 2);
                if (count($parts) === 2) {
                    $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
                }
                return strlen($line);
            },
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }
        $raw = curl_exec($ch);
        if ($raw === false) {
            throw new RuntimeException(curl_error($ch));
        }
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        if ($status >= 400) {
            $error = json_decode($raw, true)['error'] ?? ['code' => 'http_error', 'message' => substr($raw, 0, 200)];
            $retry = isset($responseHeaders['retry-after']) ? (int) $responseHeaders['retry-after'] : null;
            throw new DocBuilderException(
                $status, $error['code'], $error['message'], $error['detail'] ?? [], $error['request_id'] ?? null, $retry
            );
        }
        return [$status, $raw];
    }
}
// DocBuilder.cs: minimal wrapper for the O3O DocBuilder API v1 (.NET 8, no extra packages).
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

public sealed class DocBuilderException : Exception
{
    public DocBuilderException(int status, string code, string message, JsonNode? detail = null,
        string? requestId = null, int? retryAfter = null) : base($"{status} {code}: {message}")
    {
        Status = status;
        Code = code;
        Detail = detail;
        RequestId = requestId;
        RetryAfter = retryAfter;
    }

    public int Status { get; }
    public string Code { get; }
    public JsonNode? Detail { get; }
    public string? RequestId { get; }
    public int? RetryAfter { get; }
}

public sealed class DocBuilder : IDisposable
{
    private readonly HttpClient _http;

    public DocBuilder(string baseUrl = "http://localhost:8080", string apiKey = "O3O_DEMO_KEY")
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/"), Timeout = TimeSpan.FromSeconds(90) };
        _http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
    }

    public void Dispose() => _http.Dispose();

    public Task<JsonNode> LimitsAsync() => GetJsonAsync("v1/limits");

    public Task<JsonNode> FormatsAsync() => GetJsonAsync("v1/formats");

    /// <summary>Synchronous: returns the result file.</summary>
    public async Task<byte[]> ConvertAsync(string path, string to, object? options = null)
    {
        using var res = await UploadAsync(path, "file", "v1/convert", new() { ["to"] = to, ["options"] = Json(options) });
        return await res.Content.ReadAsByteArrayAsync();
    }

    /// <summary>Asynchronous: returns the job object.</summary>
    public async Task<JsonNode> SubmitConvertAsync(string path, string to, object? options = null)
    {
        using var res = await UploadAsync(path, "file", "v1/convert",
            new() { ["to"] = to, ["options"] = Json(options), ["async"] = "true" });
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<byte[]> BuildAsync(string scriptJson)
    {
        using var res = await SendAsync(HttpMethod.Post, "v1/build",
            new StringContent(scriptJson, Encoding.UTF8, "application/json"));
        return await res.Content.ReadAsByteArrayAsync();
    }

    /// <summary>Enterprise only.</summary>
    public async Task<byte[]> RenderTemplateAsync(string path, object data, string? to = null)
    {
        using var res = await UploadAsync(path, "template", "v1/template/render", new() { ["data"] = Json(data), ["to"] = to });
        return await res.Content.ReadAsByteArrayAsync();
    }

    public async Task<JsonNode> ExtractTextAsync(string path)
    {
        using var res = await UploadAsync(path, "file", "v1/extract/text", new());
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<JsonNode> ExtractMetaAsync(string path)
    {
        using var res = await UploadAsync(path, "file", "v1/extract/meta", new());
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    public async Task<byte[]> ThumbnailAsync(string path, int page = 1, int width = 320, string format = "png")
    {
        using var res = await UploadAsync(path, "file", "v1/extract/thumbnail",
            new() { ["page"] = page.ToString(), ["width"] = width.ToString(), ["format"] = format });
        return await res.Content.ReadAsByteArrayAsync();
    }

    public Task<JsonNode> JobAsync(string jobId) => GetJsonAsync($"v1/jobs/{jobId}");

    public async Task<JsonNode> WaitAsync(string jobId, TimeSpan? timeout = null)
    {
        var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromMinutes(10));
        for (var delay = 1.0; ; delay = Math.Min(delay * 1.5, 5))
        {
            var job = await JobAsync(jobId);
            var status = (string?)job["status"];
            if (status == "done") return job;
            if (status == "failed")
            {
                var error = job["error"];
                throw new DocBuilderException(0, (string?)error?["code"] ?? "failed", (string?)error?["message"] ?? "", error?["detail"]);
            }
            if (DateTime.UtcNow > deadline) throw new TimeoutException(jobId);
            await Task.Delay(TimeSpan.FromSeconds(delay));
        }
    }

    public async Task<byte[]> DownloadAsync(string fileId)
    {
        using var res = await SendAsync(HttpMethod.Get, $"v1/files/{fileId}");
        return await res.Content.ReadAsByteArrayAsync();
    }

    private static string? Json(object? value) => value is null ? null : JsonSerializer.Serialize(value);

    private async Task<JsonNode> GetJsonAsync(string path)
    {
        using var res = await SendAsync(HttpMethod.Get, path);
        return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
    }

    private async Task<HttpResponseMessage> UploadAsync(string path, string field, string endpoint, Dictionary<string, string?> fields)
    {
        var form = new MultipartFormDataContent();
        form.Add(new ByteArrayContent(await File.ReadAllBytesAsync(path)), field, Path.GetFileName(path));
        foreach (var (key, value) in fields)
            if (value is not null) form.Add(new StringContent(value), key);
        return await SendAsync(HttpMethod.Post, endpoint, form);
    }

    private async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path, HttpContent? content = null)
    {
        var res = await _http.SendAsync(new HttpRequestMessage(method, path) { Content = content });
        var status = (int)res.StatusCode;
        if (status < 400) return res;
        var raw = await res.Content.ReadAsStringAsync();
        int? retryAfter = res.Headers.RetryAfter?.Delta is TimeSpan d ? (int)d.TotalSeconds : null;
        res.Dispose();
        JsonNode? error = null;
        try { error = JsonNode.Parse(raw)?["error"]; } catch (JsonException) { }
        throw new DocBuilderException(status, (string?)error?["code"] ?? "http_error", (string?)error?["message"] ?? raw,
            error?["detail"], (string?)error?["request_id"], retryAfter);
    }
}
# o3o-docbuilder.sh: curl helpers for the O3O DocBuilder API v1 (curl 7.76+). Load with: source o3o-docbuilder.sh
O3O_URL="${O3O_URL:-http://localhost:8080}"
O3O_KEY="${O3O_KEY:-O3O_DEMO_KEY}"

# Print the JSON body; on HTTP errors print the error JSON and exit non-zero.
o3o_json() {
  curl -sS --fail-with-body -H "Authorization: Bearer $O3O_KEY" "$@"
}

# Save the body to file $1; on error print the error JSON to stderr and delete the file.
o3o_file() {
  local out="$1" code
  shift
  code=$(curl -sS -o "$out" -w '%{http_code}' -H "Authorization: Bearer $O3O_KEY" "$@") || return 1
  if [ "$code" -ge 400 ]; then cat "$out" >&2; rm -f "$out"; return 1; fi
}

o3o_convert()  { o3o_file "$3" -F "file=@$1" -F "to=$2" "$O3O_URL/v1/convert"; }
o3o_build()    { o3o_file "$2" -H "Content-Type: application/json" --data-binary "@$1" "$O3O_URL/v1/build"; }
o3o_render()   { o3o_file "$4" -F "template=@$1" -F "data=<$2" -F "to=$3" "$O3O_URL/v1/template/render"; }
o3o_text()     { o3o_json -F "file=@$1" "$O3O_URL/v1/extract/text"; }
o3o_meta()     { o3o_json -F "file=@$1" "$O3O_URL/v1/extract/meta"; }
o3o_thumb()    { o3o_file "$2" -F "file=@$1" -F "width=${3:-320}" "$O3O_URL/v1/extract/thumbnail"; }
o3o_job()      { o3o_json "$O3O_URL/v1/jobs/$1"; }
o3o_download() { o3o_file "$2" "$O3O_URL/v1/files/$1"; }
o3o_limits()   { o3o_json "$O3O_URL/v1/limits"; }

Using the wrapper#

Synchronous and asynchronous conversion, text extraction, error handling by code
import os

from o3o_docbuilder import DocBuilder, DocBuilderError

db = DocBuilder("http://localhost:8080", "O3O_DEMO_KEY")

# Synchronous: docx to PDF/A
with open("bao-cao.pdf", "wb") as fh:
    fh.write(db.convert("bao-cao.docx", "pdf", options={"pdf": {"pdfa": True}}))

# Asynchronous: submit, wait, download every file
job = db.convert("bao-cao.docx", "pdf", async_job=True)
done = db.wait(job["id"])
for output in done["outputs"]:
    with open(os.path.basename(output["filename"]), "wb") as fh:
        fh.write(db.download(output["file_id"]))

print(db.extract_text("bao-cao.docx")["page_count"])

try:
    db.render_template("hop-dong-mau.docx", {"so_hop_dong": "HD-2026-091"}, to="pdf")
except DocBuilderError as e:
    if e.code != "forbidden_feature":
        raise
    print("Template filling needs Enterprise")
import { writeFile } from "node:fs/promises";
import { basename } from "node:path";
import { DocBuilder, DocBuilderError } from "./docbuilder.mjs";

const db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

// Synchronous: docx to PDF/A
await writeFile("bao-cao.pdf", await db.convert("bao-cao.docx", "pdf", { options: { pdf: { pdfa: true } } }));

// Asynchronous: submit, wait, download every file
const job = await db.convert("bao-cao.docx", "pdf", { asyncJob: true });
const done = await db.wait(job.id);
for (const output of done.outputs) {
  await writeFile(basename(output.filename), await db.download(output.file_id));
}

console.log((await db.extractText("bao-cao.docx")).page_count);

try {
  await db.renderTemplate("hop-dong-mau.docx", { so_hop_dong: "HD-2026-091" }, { to: "pdf" });
} catch (e) {
  if (!(e instanceof DocBuilderError) || e.code !== "forbidden_feature") throw e;
  console.log("Template filling needs Enterprise");
}
<?php
require __DIR__ . '/DocBuilder.php';

$db = new DocBuilder('http://localhost:8080', 'O3O_DEMO_KEY');

// Synchronous: docx to PDF/A
file_put_contents('bao-cao.pdf', $db->convert('bao-cao.docx', 'pdf', ['pdf' => ['pdfa' => true]]));

// Asynchronous: submit, wait, download every file
$job = $db->convert('bao-cao.docx', 'pdf', [], true);
$done = $db->wait($job['id']);
foreach ($done['outputs'] as $output) {
    file_put_contents(basename($output['filename']), $db->download($output['file_id']));
}

echo $db->extractText('bao-cao.docx')['page_count'], "\n";

try {
    $db->renderTemplate('hop-dong-mau.docx', ['so_hop_dong' => 'HD-2026-091'], 'pdf');
} catch (DocBuilderException $e) {
    if ($e->errorCode !== 'forbidden_feature') {
        throw $e;
    }
    echo "Template filling needs Enterprise\n";
}
using var db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

// Synchronous: docx to PDF/A
await File.WriteAllBytesAsync("bao-cao.pdf", await db.ConvertAsync("bao-cao.docx", "pdf", new { pdf = new { pdfa = true } }));

// Asynchronous: submit, wait, download every file
var job = await db.SubmitConvertAsync("bao-cao.docx", "pdf");
var done = await db.WaitAsync((string)job["id"]!);
foreach (var output in done["outputs"]!.AsArray())
{
    var name = Path.GetFileName((string)output!["filename"]!);
    await File.WriteAllBytesAsync(name, await db.DownloadAsync((string)output["file_id"]!));
}

Console.WriteLine((await db.ExtractTextAsync("bao-cao.docx"))["page_count"]);

try
{
    await db.RenderTemplateAsync("hop-dong-mau.docx", new { so_hop_dong = "HD-2026-091" }, "pdf");
}
catch (DocBuilderException e) when (e.Code == "forbidden_feature")
{
    Console.WriteLine("Template filling needs Enterprise");
}
source ./o3o-docbuilder.sh

o3o_convert bao-cao.docx pdf bao-cao.pdf
o3o_text bao-cao.docx
o3o_thumb bao-cao.docx trang-1.png 480
o3o_build bao-gia.json bao-gia.docx
o3o_limits

Method map#

PythonJavaScriptPHPC#Endpoint
limits()limits()limits()LimitsAsync()GET /v1/limits
formats()formats()formats()FormatsAsync()GET /v1/formats
convert(), convert_url()convert(), convertUrl()convert(), convertUrl()ConvertAsync(), SubmitConvertAsync()POST /v1/convert
build()build()build()BuildAsync()POST /v1/build
render_template()renderTemplate()renderTemplate()RenderTemplateAsync()POST /v1/template/render (Enterprise)
extract_text()extractText()extractText()ExtractTextAsync()POST /v1/extract/text
extract_meta()extractMeta()extractMeta()ExtractMetaAsync()POST /v1/extract/meta
thumbnail()thumbnail()thumbnail()ThumbnailAsync()POST /v1/extract/thumbnail
job(), wait()job(), wait()job(), wait()JobAsync(), WaitAsync()GET /v1/jobs/{id}
download()download()download()DownloadAsync()GET /v1/files/{id}

Retrying when limited#

Retry only on 429 rate_limited and 503 queue_full or 503 pool_unavailable, after the number of seconds in Retry-After. Other 4xx errors are request errors: resending the same request fails the same way.

A reusable retry helper
import time

from o3o_docbuilder import DocBuilder, DocBuilderError

db = DocBuilder("http://localhost:8080", "O3O_DEMO_KEY")


def with_retry(call, attempts=5):
    for attempt in range(attempts):
        try:
            return call()
        except DocBuilderError as e:
            if e.status not in (429, 503) or attempt == attempts - 1:
                raise
            time.sleep(e.retry_after or 5)


pdf = with_retry(lambda: db.convert("bao-cao.docx", "pdf"))
import { setTimeout as sleep } from "node:timers/promises";
import { DocBuilder, DocBuilderError } from "./docbuilder.mjs";

const db = new DocBuilder("http://localhost:8080", "O3O_DEMO_KEY");

async function withRetry(call, attempts = 5) {
  for (let attempt = 0; ; attempt++) {
    try {
      return await call();
    } catch (e) {
      const retryable = e instanceof DocBuilderError && (e.status === 429 || e.status === 503);
      if (!retryable || attempt === attempts - 1) throw e;
      await sleep(1000 * (e.retryAfter ?? 5));
    }
  }
}

const pdf = await withRetry(() => db.convert("bao-cao.docx", "pdf"));