Document to PDF API

Uploads an office document, converts it to PDF with LibreOffice and stores the PDF in object storage. One request does the whole job — the conversion is synchronous, there is no job id to poll.

Quick start

curl -X POST https://file-service.dacc.app/pdf-convert \
  -H "x-api-key: $API_KEY" \
  -F "file=@report.docx"

Authentication. Every request needs the shared secret in the x-api-key header.

Endpoints

MethodPathPurpose
POST/pdf-convert Upload a document, get back a PDF in object storage
GET/health Liveness probe

POST /pdf-convert

Request

Content type
multipart/form-data
File field
file — exactly this name
Header
x-api-key: <API_KEY>
Body
the document itself
Maximum size
64 MB

Supported formats, selected by the file extension: .doc .docx .odt .xls .xlsx .ods .ppt .pptx .odp

Keep the original extension. The service picks the parser from it and checks the file content against the format it claims to be, so a renamed .pptx sent as .docx is rejected.

Response — 200

{
  "success": true,
  "key": "pdf/2f1c9d84-3c2b-4a51-9f0e-8d7c6b5a4f31-report.docx.pdf",
  "url": "https://pub-....r2.dev/pdf/2f1c9d84-...-report.docx.pdf",
  "bucket": "libreoffice-webservices",
  "size": 26890,
  "contentType": "application/pdf",
  "pageCount": 3,
  "backend": "native",
  "missingFonts": ["微軟正黑體"],
  "source": {
    "filename": "report.docx",
    "size": 1243,
    "extension": "docx"
  }
}
FieldTypeMeaning
successbooleanAlways true on success
keystringObject key of the PDF in the bucket
urlstring?Public URL of the PDF; absent when the service has no public base URL configured
bucketstringBucket the PDF was written to
sizenumberSize of the generated PDF in bytes
contentTypestringAlways application/pdf
pageCountnumber?Number of pages, when the engine reports it
backendstring?native (macOS/Windows) or wasm (Linux)
missingFontsstring[]Font families the document declared that no installed font provides under that exact name
sourceobjectThe uploaded document: original filename, byte size, normalized extension

Getting the PDF

The JSON response does not contain the PDF bytes. Two ways to get them:

  1. Fetch url — the file lives in object storage.
  2. Add ?download=1 — the response body is the PDF (Content-Type: application/pdf, Content-Disposition: attachment). The X-Object-Key and X-Object-Url headers carry the same information as the JSON fields.
curl -X POST "https://file-service.dacc.app/pdf-convert?download=1" \
  -H "x-api-key: $API_KEY" \
  -F "file=@report.docx" \
  -o report.pdf

Errors

Every error has the same shape:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Unsupported file type. Supported extensions: doc, docx, ...",
  "path": "/pdf-convert",
  "timestamp": "2026-10-26T03:19:08.541Z"
}
StatusCauseWhat to do
400No file, unsupported extension, unusable filenameFix the request; do not retry unchanged
401Missing or wrong x-api-keyFix the credential; do not retry
413Upload larger than the limitSplit or shrink the document
415File content does not match its extensionSend a real office document, not a renamed file
500The document could not be parsedThe file is malformed or of a type the engine rejects
503The service is busy, or the engine is unavailableRetry with backoff; a busy queue clears on its own

A conversion of a 3 MB deck takes a few seconds. Give the request a generous timeout (120 s or more) and treat a timeout as retryable.

Python

import os, requests

BASE = os.environ.get("PDF_SERVICE_URL", "https://file-service.dacc.app")

response = requests.post(
    f"{BASE}/pdf-convert",
    headers={"x-api-key": os.environ["API_KEY"]},
    files={"file": open("report.docx", "rb")},
    timeout=120,
)
response.raise_for_status()

result = response.json()
print(result["key"], result["pageCount"], result["missingFonts"])

pdf = requests.get(result["url"], timeout=60)   # or use ?download=1

Checklist