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
| Method | Path | Purpose |
|---|---|---|
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"
}
}
| Field | Type | Meaning |
|---|---|---|
success | boolean | Always true on success |
key | string | Object key of the PDF in the bucket |
url | string? | Public URL of the PDF; absent when the service has no public base URL configured |
bucket | string | Bucket the PDF was written to |
size | number | Size of the generated PDF in bytes |
contentType | string | Always application/pdf |
pageCount | number? | Number of pages, when the engine reports it |
backend | string? | native (macOS/Windows) or wasm (Linux) |
missingFonts | string[] | Font families the document declared that no installed font provides under that exact name |
source | object | The 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:
- Fetch
url— the file lives in object storage. - Add
?download=1— the response body is the PDF (Content-Type: application/pdf,Content-Disposition: attachment). TheX-Object-KeyandX-Object-Urlheaders 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"
}
| Status | Cause | What to do |
|---|---|---|
| 400 | No file, unsupported extension, unusable filename | Fix the request; do not retry unchanged |
| 401 | Missing or wrong x-api-key | Fix the credential; do not retry |
| 413 | Upload larger than the limit | Split or shrink the document |
| 415 | File content does not match its extension | Send a real office document, not a renamed file |
| 500 | The document could not be parsed | The file is malformed or of a type the engine rejects |
| 503 | The service is busy, or the engine is unavailable | Retry 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
- Send the API key on every request.
- Use the multipart field name
file. - Keep the original file extension.
- Fetch
url(or use?download=1) — the JSON response has no PDF bytes. - Retry 503 with backoff; never retry 400, 401, 413 or 415 unchanged.
- Read
missingFontsif the layout of the result matters.