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.
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: 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#
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_limitsMethod map#
| Python | JavaScript | PHP | C# | 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.
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"));