Skip to content

O3O DocBuilder and conversion

Extracting text, metadata and thumbnails

Three synchronous endpoints pull content out of documents: POST /v1/extract/text for plain text per page, POST /v1/extract/meta for metadata, POST /v1/extract/thumbnail for an image of one page.

On this page

Extraction is always synchronous, creates no job and works on both plans. All three endpoints accept every source format in the format matrix, sent as multipart (field file) or JSON (field url). Text and thumbnails go through an intermediate PDF and then pdftotext or pdftoppm; metadata is read through UNO without rendering pages.

Extracting text#

POST/v1/extract/text

Extract plain text, page by page.

Auth: BearerCommunityEnterprise

multipart/form-data body

  • filefilerequired
    Source file.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.
  • max_charsinteger 1–5.000.000optionalDefault: 1000000
    Truncate the returned text at this many characters.

application/json body

  • urlURL, ≤ 2048required
    URL of the source file, subject to the SSRF rules.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.
  • max_charsinteger 1–5.000.000optionalDefault: 1000000
    Truncate the returned text at this many characters.
Extract the text of a docx file
curl -sS http://localhost:8080/v1/extract/text \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@bao-gia.docx"
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-gia.docx")]), "bao-gia.docx");

const res = await fetch(`${BASE_URL}/v1/extract/text`, {
  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-gia.docx", "rb") as f:
    r = requests.post(
        f"{BASE_URL}/v1/extract/text",
        headers=HEADERS,
        files={"file": ("bao-gia.docx", f)},
        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/extract/text");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('bao-gia.docx'),
    ],
    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-gia.docx")), "file", "bao-gia.docx");

using var res = await http.PostAsync("http://localhost:8080/v1/extract/text", form);
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());
200Full text and per-page text (content shortened in this example).
{
  "text": "BÁO GIÁ BẢN QUYỀN\nSố: BG-2026-0917  ·  Ngày lập: 21/09/2026\n1. Thông tin khách hàng\nKính gửi: Công ty TNHH Thương mại và Dịch vụ An Phát\n\nPhụ lục: Cách đếm kết nối\nMột người mở ba tài liệu ở chế độ sửa được tính là ba kết nối.",
  "pages": [
    {
      "page": 1,
      "text": "BÁO GIÁ BẢN QUYỀN\nSố: BG-2026-0917  ·  Ngày lập: 21/09/2026\n1. Thông tin khách hàng\nKính gửi: Công ty TNHH Thương mại và Dịch vụ An Phát"
    },
    {
      "page": 2,
      "text": "Phụ lục: Cách đếm kết nối\nMột người mở ba tài liệu ở chế độ sửa được tính là ba kết nối."
    }
  ],
  "page_count": 2,
  "chars": 226,
  "truncated": false
}
  • text is the whole text with pages joined by two line breaks, truncated at max_chars.
  • chars is the length of the FULL text before truncation; truncated says whether it was cut.
  • pages gives the text of each page, handy for page-level search indexing.

Common errors: 400 bad_request when neither file nor url is sent or max_chars is out of range, 415 unsupported_format, 422 corrupt_source, 422 password_required. The full table is at the end of the page.

Metadata#

POST/v1/extract/meta

Extract document metadata and statistics.

Auth: BearerCommunityEnterprise

multipart/form-data body

  • filefilerequired
    Source file.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.

application/json body

  • urlURL, ≤ 2048required
    URL of the source file, subject to the SSRF rules.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.
Metadata of a spreadsheet from a URL
curl -sS http://localhost:8080/v1/extract/meta \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/files/bang-luong.xlsx"}'
const BASE_URL = "http://localhost:8080";
const HEADERS = { Authorization: "Bearer O3O_DEMO_KEY" };

const payload = {"url": "https://example.com/files/bang-luong.xlsx"};

const res = await fetch(`${BASE_URL}/v1/extract/meta`, {
  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()}`);
console.log(await res.json());
import requests

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

payload = {"url": "https://example.com/files/bang-luong.xlsx"}
r = requests.post(f"{BASE_URL}/v1/extract/meta", headers=HEADERS, json=payload, timeout=90)
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
print(r.json())
<?php
$payload = json_encode(['url' => 'https://example.com/files/bang-luong.xlsx'], JSON_UNESCAPED_UNICODE);

$ch = curl_init("http://localhost:8080/v1/extract/meta");
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));
}
print_r(json_decode($body, true));
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/bang-luong.xlsx"
    }
    """;
using var res = await http.PostAsync("http://localhost:8080/v1/extract/meta",
    new StringContent(json, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
Console.WriteLine(await res.Content.ReadAsStringAsync());
200Spreadsheet: sheets lists the sheet names; text statistics are null.
{
  "family": "sheet",
  "format": "xlsx",
  "size": 9876,
  "title": "Bảng lương tháng 9 năm 2026",
  "subject": null,
  "author": "Phòng Hành chính Nhân sự",
  "description": null,
  "keywords": [],
  "created": "2026-09-21T10:00:00Z",
  "modified": "2026-09-21T10:00:00Z",
  "generator": "LibreOffice/7.4.7.2$Linux_X86_64",
  "pages": null,
  "words": null,
  "chars": null,
  "paragraphs": null,
  "sheets": [
    "Bảng lương",
    "Theo phòng ban"
  ],
  "slides": null
}

Result fields

  • familytext | sheet | sliderequired
    Document family.
  • formatstringrequired
    Detected source format.
  • sizeintegerrequired
    Source file size in bytes.
  • title, subject, author, descriptionstring | nulloptional
    Document properties.
  • keywordsarrayoptional
    Keywords.
  • created, modifieddate-time | nulloptional
    Creation and modification time.
  • generatorstring | nulloptional
    Software that produced the file.
  • pagesinteger | nulloptional
    Page count (text family), null otherwise.
  • words, chars, paragraphsinteger | nulloptional
    Document statistics.
  • sheetsarrayoptional
    Sheet names (sheet family), an empty array otherwise.
  • slidesinteger | nulloptional
    Slide count (slide family).

Common errors: 401 unauthorized, 413 file_too_large, 415 unsupported_format, 422 corrupt_source, 422 password_required; with a URL source also 422 url_not_allowed and 422 download_failed.

Thumbnails#

POST/v1/extract/thumbnail

A PNG or JPG image of one page, returned directly.

Auth: BearerCommunityEnterprise

multipart/form-data body

  • filefilerequired
    Source file.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.
  • pageinteger ≥ 1optionalDefault: 1
    Page to render.
  • widthinteger 16–2000optionalDefault: 320
    Image width in pixels.
  • formatpng | jpgoptionalDefault: png
    Image format.

application/json body

  • urlURL, ≤ 2048required
    URL of the source file, subject to the SSRF rules.
  • fromstringoptional
    Source format when the file name has no clear extension.
  • passwordstringoptional
    Password to open the source file.
  • pageinteger ≥ 1optionalDefault: 1
    Page to render.
  • widthinteger 16–2000optionalDefault: 320
    Image width in pixels.
  • formatpng | jpgoptionalDefault: png
    Image format.
A 320-pixel-wide image of the title slide
curl -sS http://localhost:8080/v1/extract/thumbnail \
  -H "Authorization: Bearer O3O_DEMO_KEY" \
  -F "file=@gioi-thieu.pptx" \
  -F "page=1" \
  -F "width=320" \
  -F "format=png" \
  -D - \
  -o trang-bia.png -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("gioi-thieu.pptx")]), "gioi-thieu.pptx");
form.append("page", "1");
form.append("width", "320");
form.append("format", "png");

const res = await fetch(`${BASE_URL}/v1/extract/thumbnail`, {
  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-bia.png", Buffer.from(await res.arrayBuffer()));
console.log("X-O3O-Page-Count:", res.headers.get("X-O3O-Page-Count"));
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/extract/thumbnail",
        headers=HEADERS,
        files={"file": ("gioi-thieu.pptx", f)},
        data={
            "page": "1",
            "width": "320",
            "format": "png",
        },
        timeout=90,
    )
if not r.ok:
    raise RuntimeError(f"{r.status_code}: {r.text}")
with open("trang-bia.png", "wb") as fh:
    fh.write(r.content)
print("X-O3O-Page-Count:", r.headers.get("X-O3O-Page-Count"))
<?php
$headers = [];
$ch = curl_init("http://localhost:8080/v1/extract/thumbnail");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer O3O_DEMO_KEY"],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('gioi-thieu.pptx'),
        'page' => '1',
        'width' => '320',
        'format' => 'png',
    ],
    CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
        $parts = explode(":", $line, 2);
        if (count($parts) === 2) {
            $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
        }
        return strlen($line);
    },
    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("trang-bia.png", $body);
echo "X-O3O-Page-Count: " . ($headers["x-o3o-page-count"] ?? "") . "\n";
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("gioi-thieu.pptx")), "file", "gioi-thieu.pptx");
form.Add(new StringContent("1"), "page");
form.Add(new StringContent("320"), "width");
form.Add(new StringContent("png"), "format");

using var res = await http.PostAsync("http://localhost:8080/v1/extract/thumbnail", form);
if (!res.IsSuccessStatusCode)
    throw new HttpRequestException($"{(int)res.StatusCode}: {await res.Content.ReadAsStringAsync()}");
await File.WriteAllBytesAsync("trang-bia.png", await res.Content.ReadAsByteArrayAsync());
Console.WriteLine("X-O3O-Page-Count: " + string.Join(",", res.Headers.GetValues("X-O3O-Page-Count")));
200The body is the image. The X-O3O-Page-Count header gives the total page count, handy for page navigation when showing thumbnails.
{
  "X-O3O-Request-Id": "req_0123456789abcdef",
  "X-O3O-Page-Count": "3",
  "X-RateLimit-Limit": "10",
  "X-RateLimit-Remaining": "9",
  "X-RateLimit-Reset": "1789984860"
}

Common errors#

HTTPCodeWhen
400bad_requestNeither file nor url, or page beyond the page count.
401unauthorizedMissing or wrong key. detail.reason is missing, invalid or no_keys_configured.
413file_too_largeInput or output file exceeds max_file_mb. detail.limit_mb.
415unsupported_formatSource format not recognised, or the source/target pair is not supported.
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.
503pool_unavailableNo worker is alive.
504timeoutSynchronous mode exceeded sync_timeout_seconds, or the job exceeded job_timeout_seconds. The worker is killed and restarted.
400Page 9 requested from a 3-page document.
{
  "error": {
    "code": "bad_request",
    "message": "Tham số không hợp lệ.",
    "detail": {
      "errors": [
        {
          "path": "/page",
          "message": "Trang 9 vượt số trang của tài liệu (3)."
        }
      ]
    },
    "request_id": "req_0123456789abcdef"
  }
}

Coming later#

Coming soon

Document structure extraction (/v1/extract/structure), embedded media extraction (/v1/extract/media) and comparing two documents (/v1/compare).