API manual
Jason Tools Document Toolbox exposes a RESTful API. Every tool has an endpoint, ready to plug into automation, your own systems or scheduled jobs.
Basic principle: the API is open by default (matching the web UI); to lock it, an administrator turns on enforce mode on the admin “API Token” page and issues bearer tokens.
Authentication disabled (default)
A new installation has authentication off, so every /api/* endpoint works straight away without a token.
Enabling API tokens
An administrator goes to admin → API Token:
- Press "issue a new token" → give it a purpose (for example
gitlab-ci) → receive a 64-character hex token (shown once only; keep it safe) - Tick “Enforce; refuse anything without a token” and save
From then on every /api/* call must carry one of these:
Authorization: Bearer 64char-hex-token-here
or as a query string:
GET /api/jobs/abc123?token=64char-hex-token-here
Missing or invalid token → 401 Unauthorized JSON:
{"ok": false, "detail": "需要有效的 API token(Authorization: Bearer ...)"}
Tokens are managed from admin /
jtdtManaged from the CLI, entirely separate from web authentication (jtdt-admin/ LDAP / AD).
2. Common conventions
| Item | Description |
|---|---|
| Base URL | http://your-server:8765 (depends on JTDT_HOST / JTDT_PORT ; the examples below use localhost:8765) |
| Content-Type | File uploads: multipart/form-data; JSON: application/json |
| Authentication header | -H "Authorization: Bearer YOUR_TOKEN" (may be omitted while enforce is off) |
| Response format | JSON (unless it explicitly returns PDF / PNG / ZIP binary data) |
| Error format | {"detail": "錯誤訊息"} plus the matching HTTP 4xx/5xx |
| Large files | Large or slow operations use job mode: the call first returns {"job_id": "..."}, you poll /api/jobs/{job_id}, and once it is done you fetch the result from /api/jobs/{job_id}/download |
Each endpoint below includes
curlexamples. Endpoints that return a file use--output 檔名to save it; endpoints that return JSON can be piped through| jqto pretty-print it.
Office to PDF
Convert an office document to PDF (via the OxOffice / LibreOffice engine).
POST /api/convert-to-pdf
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | Office document |
curl -X POST http://localhost:8765/api/convert-to-pdf \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@報告.docx" \
--output 報告.pdf
Response: the PDF itself (application/pdf). On failure, 4xx plus JSON {"detail": "..."}.
Office format conversion
Converts between formats of the same kind: word processing ↔ word processing, spreadsheet ↔ spreadsheet, presentation ↔ presentation. Job mode; returns job_id.
Ask which target formats are available before submitting a conversion. The values of target differ from one installation to another (without the Impress module there is no presentation group), so do not hard-code the ids in your own program:
GET /tools/office-convert/formats
curl -s http://localhost:8765/tools/office-convert/formats \
-H "Authorization: Bearer YOUR_TOKEN" | jq
# → {"families": [{"id": "text", "name": "文書檔",
# "sources": ["odt", "docx", ...],
# "targets": [{"id": "docx-2007", "ext": "docx",
# "label": "Word 2007", "note": "...",
# "common": true}, ...]}, ...]}
POST /tools/office-convert/convert
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | Office document; may be repeated (several files come back as a ZIP) |
target | str | ✓ | The target format id, taken from formats above |
Files in one request must all be of the same kind (a spreadsheet and a word-processing file cannot be mixed) and must match the kind that target belongs to, otherwise the call returns 400.
# 文書檔:.odt 轉成 Word 97–2003
curl -X POST http://localhost:8765/tools/office-convert/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@報告.odt" -F "target=doc" | jq
# → {"job_id": "...", "download_url": "/api/jobs/.../download"}
# 簡報:.pptx 轉成 ODF 簡報
curl -X POST http://localhost:8765/tools/office-convert/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@簡報.pptx" -F "target=odp" | jq
.docx / .xlsx / .pptx each offer two targets; the difference is the Compatibility mode is more than just a name:
| Target id | Produces | Compatibility mode |
|---|---|---|
docx-2007 | Word 2007 format | 12 |
docx-365 | Word 2010; 365 format | 15 |
xlsx-2007 / xlsx-ooxml | Excel 2007–365 / Office Open XML | — |
pptx-2007 / pptx-ooxml | PowerPoint 2007–365 / Office Open XML | — |
Response: {"job_id": "...", "download_url": "..."}. One file comes back in the target format, several come back as a ZIP. On failure, 4xx plus JSON {"detail": "..."}.
Images to PDF
Combine one or more images into a single PDF.
POST /tools/image-to-pdf/api/image-to-pdf
| Parameter | Type | Required | Description |
|---|---|---|---|
files | file (repeatable) | ✓ | PNG / JPG / GIF / TIFF / WebP / HEIC |
page_size | str | A4 (default) / A3 / A5 / B5 / Letter / Legal / Tabloid / original | |
margin_mm | float | Margin (mm), default 0 | |
rotations | str | Rotation for each image as CSV, in upload order, e.g. 0,90,0 | |
filename | str | Output filename |
curl -X POST http://localhost:8765/tools/image-to-pdf/api/image-to-pdf \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@p1.jpg" -F "files=@p2.png" \
-F "page_size=A4" -F "margin_mm=5" \
--output album.pdf
Response: PDF binary.
PDF to images
Turn every page of a PDF into a PNG (several pages come back as a ZIP).
POST /tools/pdf-to-image/convert
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
dpi | int | Resolution, default 150 |
curl -X POST http://localhost:8765/tools/pdf-to-image/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" -F "dpi=200" \
--output pages.zip
Response: a PNG for one page, a ZIP for several.
PDF to Office
Turns a PDF back into Word (.docx) or OpenDocument (.odt). Job mode; returns job_id.
POST /tools/pdf-to-office/convert
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
output_format | str | docx (default) / odt | |
engine | str | Conversion engine: pdf2docx-refine (default, stable) / jtdt-reform (our own layout rebuild) / jtdt-layout (layout reproduction: precise import through LibreOffice/OxOffice Draw, then rebuilt by our own engine) | |
enable_postprocess | bool | Only applies to pdf2docx-refine: whether to run the jtdt-refine post-processing (25 fixers), default false |
# 預設引擎 pdf2docx-refine
curl -X POST http://localhost:8765/tools/pdf-to-office/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@form.pdf" -F "output_format=docx" \
| jq
# → {"job_id": "...", "download_url": "/api/jobs/.../download"}
# 改用自家 jtdt-reform 引擎
curl -X POST http://localhost:8765/tools/pdf-to-office/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@form.pdf" -F "output_format=odt" -F "engine=jtdt-reform" \
| jq
# 改用 jtdt-layout 版面重現引擎(表單 / 多欄 / 含框線表格版面最接近原 PDF)
curl -X POST http://localhost:8765/tools/pdf-to-office/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@form.pdf" -F "output_format=odt" -F "engine=jtdt-layout" \
| jq
Response: {"job_id": "...", "download_url": "..."}; then poll and fetch with the job API in chapter 10.
Getting before/after thumbnails: once the job finishes, meta.preview in GET /api/jobs/{job_id} contains page_indices (the 0-based pages to preview: all of them for ≤ 6 pages, otherwise the first 2, middle 2 and last 2), orig_pages, result_pages, orig_chars and result_chars. For the thumbnails themselves:
GET /tools/pdf-to-office/preview/{job_id}/orig/{page} # 轉換前(原 PDF)
GET /tools/pdf-to-office/preview/{job_id}/result/{page} # 轉換後(docx/odt 渲染)
page is a 1-based page number (the page_indices element plus 1), returning image/png。
# 完成後讀 preview 頁碼清單
curl -s http://localhost:8765/api/jobs/$JOB \
-H "Authorization: Bearer YOUR_TOKEN" | jq '.meta.preview'
# 取第 1 頁的前 / 後對照縮圖
curl -s http://localhost:8765/tools/pdf-to-office/preview/$JOB/orig/1 \
-H "Authorization: Bearer YOUR_TOKEN" --output before_p1.png
curl -s http://localhost:8765/tools/pdf-to-office/preview/$JOB/result/1 \
-H "Authorization: Bearer YOUR_TOKEN" --output after_p1.png
PDF to Markdown
Extract a PDF as Markdown. A single call returns the content directly, without job mode.
POST /tools/pdf-to-markdown/api/pdf-to-markdown
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
include_images | bool | Whether to output the embedded images as well, default false | |
page_separator | bool | Insert a separator between pages, default true | |
image_format | str | Image format png (default) or jpg; only applies when include_images=true |
# 只要文字:回 text/markdown
curl -X POST http://localhost:8765/tools/pdf-to-markdown/api/pdf-to-markdown \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@report.pdf" --output report.md
# 連圖片一起:回 ZIP(.md + 圖片檔)
curl -X POST http://localhost:8765/tools/pdf-to-markdown/api/pdf-to-markdown \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@report.pdf" -F "include_images=true" --output report.zip
The response type depends on include_images: false returns text/markdown, true returns application/zip
Markdown to office document
Turns Markdown into PDF / Word / OpenDocument. The content can be uploaded as file or passed directly as text.
POST /tools/markdown-to-doc/api/markdown-to-doc
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | △ | A Markdown file. Use this or text |
text | str | △ | The Markdown content itself. Use this or file |
format | str | pdf (default) / docx / odt. Any other value returns 400 | |
theme | str | Layout theme, default classic | |
font | str | Font, default default | |
title | str | Document title |
# 檔案轉 PDF
curl -X POST http://localhost:8765/tools/markdown-to-doc/api/markdown-to-doc \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@notes.md" -F "format=pdf" --output notes.pdf
# 直接帶內容轉 Word
curl -X POST http://localhost:8765/tools/markdown-to-doc/api/markdown-to-doc \
-H "Authorization: Bearer YOUR_TOKEN" \
--form-string "text=# 標題
內文。" -F "format=docx" --output notes.docx
Returns the file itself, not JSON. A format other than pdf / docx / odt returns 400.
PDF to slides
Turns a PDF back into PowerPoint (.pptx) or an OpenDocument presentation (.odp), one page per slide, keeping the original slide size (portrait PDFs are reproduced too). Job mode; returns job_id.
There is only one engine (jtdt-layout layout reproduction), so there is no engine parameter.
POST /tools/pdf-to-slides/convert
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
output_format | str | pptx (default) / odp |
curl -X POST http://localhost:8765/tools/pdf-to-slides/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@deck.pdf" -F "output_format=pptx" \
| jq
# → {"job_id": "...", "download_url": "/api/jobs/.../download"}
# 輸出 OpenDocument 簡報(物件多時比 .pptx 快)
curl -X POST http://localhost:8765/tools/pdf-to-slides/convert \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@deck.pdf" -F "output_format=odp" \
| jq
Before-and-after thumbnails work the same as for pdf-to-office:
GET /tools/pdf-to-slides/preview/{job_id}/orig/{page} # 轉換前(原 PDF)
GET /tools/pdf-to-slides/preview/{job_id}/result/{page} # 轉換後(簡報渲染)
Once the job finishes, meta.stats in GET /api/jobs/{job_id} contains pages (the number of slides), images and objects (the total object count).
Environment requirement: the office suite's Impress module(
oxoffice-impress/libreoffice-impress). Without the module you only see "converted successfully but no output found"; check it under "settings → dependency check".
Merge PDFs
Merge several PDFs into one, in upload order.
POST /tools/pdf-merge/api/pdf-merge
| Parameter | Type | Required | Description |
|---|---|---|---|
files | file (repeatable) | ✓ | two or more PDFs |
curl -X POST http://localhost:8765/tools/pdf-merge/api/pdf-merge \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@a.pdf" -F "files=@b.pdf" -F "files=@c.pdf" \
--output merged.pdf
Response: the merged PDF.
Split PDF
Split a PDF by page count or ranges.
POST /tools/pdf-split/api/pdf-split
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
mode | str | ✓ | ranges (by range) / every (one file every N pages) / single (one file per page) |
ranges | str | The range when mode=ranges, e.g. 1-3,5,7-9; a number N when mode=every |
curl -X POST http://localhost:8765/tools/pdf-split/api/pdf-split \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@big.pdf" -F "mode=ranges" -F "ranges=1-3,8-10" \
--output parts.zip
Response: the resulting PDFs (several come back as a ZIP).
PDF page operations
Delete, extract or reorder pages.
POST /tools/pdf-pages/api/pdf-pages
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
mode | str | ✓ | keep (keep only) / delete (delete) / reorder (reorder) |
spec | str | ✓ | Page specification, e.g. 1,3,5-8 |
curl -X POST http://localhost:8765/tools/pdf-pages/api/pdf-pages \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" -F "mode=keep" -F "spec=1,3,5-8" \
--output picked.pdf
Response: the processed PDF.
Rotate PDF
Rotate selected pages.
POST /tools/pdf-rotate/api/pdf-rotate
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
angle | int | ✓ | 90 / 180 / 270 |
pages | str | Pages to apply it to, e.g. 1,3-5; blank = all |
curl -X POST http://localhost:8765/tools/pdf-rotate/api/pdf-rotate \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@scan.pdf" -F "angle=90" -F "pages=1-4" \
--output rotated.pdf
Response: the rotated PDF.
Add page numbers
Add page numbers at a chosen position.
POST /tools/pdf-pageno/api/pdf-pageno
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
position | str | bottom-center (default) / bottom-right / bottom-left / top-* | |
fmt | str | Format template, e.g. Page {n} / {n} / {total} | |
start | int | Starting page number, default 1 | |
font_size | float | Font size, default 10 | |
margin_mm | float | Margin (mm) | |
color | str | Text colour as hex, e.g. #000000 |
curl -X POST http://localhost:8765/tools/pdf-pageno/api/pdf-pageno \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@report.pdf" -F "position=bottom-center" \
-F "fmt=第 {n} / {total} 頁" -F "start=1" \
--output numbered.pdf
Response: the numbered PDF.
PDF page borders
Draw a border on every page. Accepts PDF and word processing files (office documents are converted to PDF first); the output is always PDF.
POST /tools/pdf-border/api/pdf-border
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | PDF or word processing file |
mode | str | page (inset from the page edge, default) / content (flush with that page's content) | |
margin_mm | float | Margin (mm), default 5 | |
width_pt | float | Line width (pt), default 1.5 | |
color | str | Border colour as hex, default #333333 | |
style | str | solid (default) / dashed / dotted | |
radius_mm | float | Corner radius (mm), default 0 (square corners) | |
opacity | float | Opacity 0; 1, default 1 | |
double | bool | Double border (certificate style), default false | |
double_gap_mm | float | Gap between the two lines of a double border (mm), default 1.5 | |
shadow | bool | Outer shadow, default false | |
shadow_color | str | Shadow colour as hex, default #000000 | |
shadow_blur_mm | float | Shadow spread (mm), default 1.2 | |
shadow_offset_mm | float | Shadow offset (mm), default 0.6 | |
shadow_opacity | float | Shadow density 0; 1, default 0.25 | |
pages | str | Specific pages, e.g. 1,3,5-8; blank = all | |
skip_first | bool | No border on the first page (a slide cover is often a full-bleed design), default false |
curl -X POST http://localhost:8765/tools/pdf-border/api/pdf-border \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@slides.pptx" -F "mode=page" -F "margin_mm=4" \
-F "width_pt=2" -F "color=#1e293b" -F "skip_first=true" \
--output slides_border.pdf
Response: the bordered PDF. Every value is clamped to a safe range on the server.
PDF bookmarks and contents
Adds bookmarks to a PDF (the navigation panel on the left of the reader) and a clickable table of contents page. Several files are joined automatically, with each file name becoming a first-level bookmark; bookmarks inside each document move one level down and are kept. Accepts PDFs and office documents (office documents are converted to PDF first).
POST /tools/pdf-bookmark/api/pdf-bookmark
| Parameter | Type | Required | Description |
|---|---|---|---|
files | file[] | ✓ | One or more PDF / office files; several are joined in order |
bookmarks | str | Supply the bookmarks yourself (a JSON array of {"title","page","level"}). If given, this replaces the automatically generated ones | |
auto | bool | Detect headings automatically from the font size, default false (only applies with no bookmarks and a single file) | |
toc_page | bool | Insert a table of contents page, default false. The web interface has this ticked by default, while the API keeps false so that existing automated calls do not suddenly gain a page | |
toc_at | int | Insert the contents before this page, default 1 (right at the front). With a cover page, use 2 | |
toc_title | str | Table of contents title, default Contents | |
toc_max_level | int | How many levels the contents lists (1; 3), default 3 |
Bookmark levels must start at 1 and may only go one level deeper at a time (1→3 is corrected to 1→2), and a page number beyond the end of the document is clamped to the last page; both are corrected automatically rather than failing. Page numbers shift on their own once a contents page is inserted, so there is nothing to work out by hand; bookmarks before toc_at are left untouched (the one pointing at the cover is still page 1).
# 多檔串接 + 以檔名建書籤 + 產生目錄頁
curl -X POST http://localhost:8765/tools/pdf-bookmark/api/pdf-bookmark \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@投標須知.pdf" -F "files=@規格書.pdf" -F "files=@價格標.pdf" \
-F "toc_page=true" \
--output 標案文件.pdf
# 單檔 + 自己指定書籤
curl -X POST http://localhost:8765/tools/pdf-bookmark/api/pdf-bookmark \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@report.pdf" \
-F 'bookmarks=[{"title":"第一章","page":1,"level":1},{"title":"1.1 背景","page":3,"level":2}]' \
--output report_bm.pdf
Response: the bookmarked PDF.
PDF seam stamp
Split one stamp across consecutive pages; if any page is swapped or lost, that slice no longer lines up. Accepts PDF and office files.
POST /tools/pdf-seam-stamp/api/pdf-seam-stamp
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | A PDF or office document, at least 2 pages |
stamp | file | Your own stamp image (PNG / JPG; a near-white background is removed automatically). Without one the system generates a stamp | |
text | str | The text on a generated stamp, default 騎縫章 (up to 4 characters, laid out in a square) | |
shape | str | circle (default) / square / rect | |
color | str | Stamp colour as hex, default #c81414 | |
mode | str | side (side seam, default) / spread (across a spread) | |
group | int | How many pages one stamp spans, default 2; 0 = one stamp for the whole document | |
edge | str | Which edge to place it on in side mode: right (default) or left | |
size_mm | float | Stamp size (mm), default 40 | |
offset_mm | float | Distance from the page edge (mm), default 3 | |
pos_mm | float | Vertical position (mm), 0 = centred | |
angle_deg | float | Angle, default 0 | |
opacity | float | Density 0; 1, default 1 | |
jitter_pos | bool | Randomise the height of each group, default false | |
jitter_angle | bool | Randomise the angle of each group (up to ±8 degrees), default false | |
seed | int | Random seed; 0 = random |
The whole stamp is rotated before it is sliced; the other way round (slice first, rotate each piece) leaves seams that do not line up. Within one group the position and angle of every slice are identical, otherwise the pieces do not fit back together.
curl -X POST http://localhost:8765/tools/pdf-seam-stamp/api/pdf-seam-stamp \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@contract.pdf" -F "text=節省" -F "group=3" \
-F "mode=side" -F "size_mm=45" -F "jitter_angle=true" \
--output contract_seam.pdf
Response: the PDF with the seam stamp.
Scan cleanup
Straightens documents that were photographed or scanned at an angle, trims dark borders and evens out uneven background. For phone photos it finds the four corners of the sheet and corrects the perspective. No AI and no GPU involved (classical image processing; about 0.8 s per page on CPU at 200 dpi).
POST /tools/doc-straighten/api/doc-straighten
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | PDF, an image (including HEIC from a phone) or an office document |
dpi | int | Processing resolution: 150 / 200 (default) / 300 | |
binarize | bool | Convert to black and white. Off by default: measurements show it erases thin Chinese strokes and clearly lowers OCR accuracy; its purpose is a smaller file, not clearer text | |
detect_quad | bool | Detect the sheet edges and correct the perspective (on by default). Falls back to straightening only when the edges cannot be found | |
enhance | bool | Clean up: flatten uneven background and shadows (on by default). Measured on a phone photo with a hard one-sided shadow, text recognition went from 0.472 to 0.982; scans that are already even are left bit-for-bit identical |
curl -X POST http://localhost:8765/tools/doc-straighten/api/doc-straighten \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@scan.pdf" -F "dpi=200" \
-o straightened.pdf -D -
# 回應標頭:
# X-Straighten-Pages: 12
# X-Straighten-Worst-Residual: 0.10 ← 修正後殘留的歪斜角(越接近 0 越好)
X-Straighten-Worst-Residual is the acceptance figure: if the rotation went the wrong way the reported angle still looks like something happened; only the residual reveals it. It should stay within 0.2 degrees.
The web interface runs it as a background job: POST /tools/doc-straighten/load to upload, POST /tools/doc-straighten/submit to start (returns a job_id), poll GET /api/jobs/{job_id}, then fetch the result from GET /api/jobs/{job_id}/download. Per-page preview is POST /tools/doc-straighten/preview, which returns the correction angle and the residual.
Unify PDF page size
Bring mixed page sizes onto one paper size. The content stays vector (text is still selectable) rather than being turned into images.
POST /tools/pdf-page-size/api/pdf-page-size
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | PDF or word processing file |
paper | str | a3 / a4 (default) / a5 / b4 / b5 / letter / legal / tabloid / custom | |
custom_w_mm custom_h_mm | float | paper=custom width and height (mm) when | |
orientation | str | auto (follow the original page, default) / portrait / landscape | |
fit | str | scale (scale to fit, default, nothing is lost) / center (centre without scaling; anything outside is cropped) / crop (scale up to fill) | |
align | str | center (default) / top-left | |
keep_same | bool | Leave pages that are already the target size untouched, default true |
curl -X POST http://localhost:8765/tools/pdf-page-size/api/pdf-page-size \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@tender.pdf" -F "paper=a4" -F "orientation=auto" -F "fit=scale" \
--output tender_a4.pdf
Response: the resized PDF.
PDF N-up
Fit several pages onto one sheet (2-up, 4-up …).
POST /tools/pdf-nup/api/pdf-nup
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
cols | int | ✓ | Columns per sheet |
rows | int | ✓ | Rows per sheet |
paper | str | Paper size, A4 (default) / A3 / Letter … | |
orientation | str | portrait / landscape |
curl -X POST http://localhost:8765/tools/pdf-nup/api/pdf-nup \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@slides.pdf" -F "cols=2" -F "rows=2" \
-F "paper=A4" -F "orientation=landscape" \
--output 4up.pdf
Response: the N-up PDF.
Compress PDF
Make a PDF smaller.
POST /tools/pdf-compress/api/pdf-compress
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
preset | str | gentle (light) / balanced (default, balanced) / aggressive (smallest) |
curl -X POST http://localhost:8765/tools/pdf-compress/api/pdf-compress \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@large.pdf" -F "preset=balanced" \
--output small.pdf
Response: the compressed PDF.
Scan merge
Detect the areas with content in several scans (both sides of an ID, say), keep their original colour and compose them onto one white A4 PDF at their relative positions. Overlaps keep their original position rather than being rearranged (use the web interface if you need to drag them).
POST /tools/scan-merge/api/scan-merge
| Parameter | Type | Required | Description |
|---|---|---|---|
files | file (repeatable) | ✓ | Scans; PDF / PNG / JPG / TIFF / WebP, one block of content each |
whiten | bool | Whether to lift a pale grey or yellowish scan background to pure white (colour content is unaffected), default true | |
filename | str | Output file name, default scan-merge.pdf |
curl -X POST http://localhost:8765/tools/scan-merge/api/scan-merge \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@id-front.jpg" -F "files=@id-back.jpg" \
-F "whiten=true" \
--output id-merged.pdf
Response: a single white A4 PDF binary.
Returns 422 when none of the files has a detectable content block (an all-white page, say, or an all-black one from scanning with the lid open). That is not a problem with the request format: there is nothing in the images to assemble, and the message says which case it is.
Extract PDF text
Extract all text from a PDF (broken CMaps and OCR double layers are handled automatically).
POST /tools/pdf-extract-text/api/pdf-extract-text
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-extract-text/api/pdf-extract-text \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" | jq
JSON response: text per page plus the whole document.
Extract PDF images
Extract every image embedded in a PDF.
POST /tools/pdf-extract-images/api/pdf-extract-images
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-extract-images/api/pdf-extract-images \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" \
--output images.zip
Response: the images as a ZIP.
Extract PDF attachments
List or extract files embedded in a PDF.
POST /tools/pdf-attachments/api/pdf-attachments
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-attachments/api/pdf-attachments \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@with_attachments.pdf" \
--output attachments.zip
Response: the attachment list plus contents (ZIP).
PDF metadata
Read or clear PDF metadata, XMP, bookmarks, annotations and form fields.
POST /tools/pdf-metadata/api/pdf-metadata
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
clear_info | bool | Clear document info (author / title / producer …) | |
clear_xmp | bool | Clear XMP metadata | |
clear_toc | bool | Clear bookmarks / contents | |
clear_annots | bool | Clear annotations | |
clear_forms | bool | Clear form fields |
curl -X POST http://localhost:8765/tools/pdf-metadata/api/pdf-metadata \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" -F "clear_info=true" -F "clear_xmp=true" \
--output cleaned.pdf
Response: without any clear_* flag it returns the metadata as JSON; with a flag it returns the cleaned PDF.
PDF word count
Count pages, characters, words, reading time and frequent terms.
POST /tools/pdf-wordcount/api/pdf-wordcount
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | A PDF, an office document (.doc / .docx / .odt / .xls / .xlsx / .ods / .ppt / .pptx / .odp) or plain text (.txt / .md / .csv / .log / .json / .xml / .html) |
Office documents are converted to PDF before counting; that is the only way the page count and the words per page match what actually prints. This endpoint therefore needs an Office engine (OxOffice / LibreOffice) when it is given an office document.
curl -X POST http://localhost:8765/tools/pdf-wordcount/api/pdf-wordcount \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" | jq
JSON response (excerpt):
{
"filename": "document.pdf",
"page_count": 12,
"char_count": 18342,
"word_count": 3521,
"estimated_reading_minutes": 12.5,
"per_page_chars": [/* ... */],
"top_words_zh2": [/* ... */], "top_words_en": [/* ... */]
}
Meeting summary
Turns a meeting transcript into a summary, decisions, action items, risks and chapters, with a segment number on every entry.
POST /tools/meeting-summary/api/meeting-summary
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | Transcript: .vtt / .srt / .json / .txt / .md / .docx / .odt |
second_pass | string | 1 (default) runs the second review pass;0 first pass only: faster, but both misses and false positives go up | |
context | string | Meeting background (optional, up to 4000 characters): the topic, attendees and their roles, terminology, or anything else you want to tell the model |
context is used only to make sense of the transcript; it never becomes a source of items. Knowing that someone is the VP of Operations and chairs the meeting helps decide who is delegating and who is the owner, but what you write in the background did not happen in the meeting — decisions, actions, risks and open questions are always drawn from the transcript. Besides saying so in the prompt, two mechanisms enforce it: citation verification (an item must point back to transcript segments, and the background is not part of what is compared), and dropping anything that closely matches the background and matches it noticeably better than it matches the transcript. Dropped items are counted in dropped_count.
This endpoint is synchronous; a two-hour meeting takes several minutes (roughly 30-60 model requests), so raise the caller's timeout. For background processing use the web path instead (POST /upload to POST /start, then poll the job id with /api/jobs/{id}).
The LLM must be enabled in the admin area first; if it is not, this returns 503. A transcript that cannot be read returns 400 (the message says which formats are supported).
Timestamps are optional: for a plain-text transcript with no times, the summary, decisions and action items still work, but speaker_stats comes back empty and charts will not contain speaker_share or timeline; both are computed from the times, and a guessed number would be worse than none.
curl -X POST http://localhost:8765/tools/meeting-summary/api/meeting-summary \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@meeting.vtt" \
--form-string 'context=會議主題:第四季預算
與會者
王小明:財務部經理,會議主席
李美華:法務專員' | jq
JSON response (excerpt):
{
"summary": { "summary": "這場會議確認了第四季的預算…" },
"items": {
"decision": [
{ "text": "第四季預算維持原案", "segment_ids": [18, 19],
"speaker": "王小明", "quote": "那就照原案走" }
],
"action": [
{ "text": "月底前把修訂版寄給法務", "owner": "李美華",
"due": "月底", "segment_ids": [42] }
],
"risk": [], "question": []
},
"chapters": [
{ "title": "預算討論", "start_ms": 0, "end_ms": 840000, "start_seq": 1 }
],
"mindmap": [
{ "node_id": "c1", "parent_id": null, "label": "預算討論",
"type": "topic", "segment_ids": [1] }
],
"charts": ["timeline", "topic_share", "speaker_share", "mindmap"],
"speaker_stats": {
"王小明": { "speaking_ms": 512000, "percentage": 61.2,
"turn_count": 24, "average_turn_ms": 21333 }
},
"dropped_count": 3,
"llm_calls": 41,
"source": { "filename": "meeting.vtt", "segments": 186 }
}
dropped_count Yes the number of entries dropped because their citation did not match the transcript Everything extracted has to be findable in the segment it claims; if it is not, it is not kept. A high number here means the model is inventing, and changing the model helps more than tuning parameters.
PDF OCR
Run OCR on a scanned PDF and add a selectable text layer.
POST /tools/pdf-ocr/api/pdf-ocr
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
lang | str | Language, e.g. chi_tra+eng (default) / eng / chi_sim | |
dpi | int | Render resolution, default 300 | |
skip_pages_with_text | bool | Skip pages that already have a text layer, default true |
curl -X POST http://localhost:8765/tools/pdf-ocr/api/pdf-ocr \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@scan.pdf" -F "lang=chi_tra+eng" -F "dpi=300" \
--output ocr.pdf
Response: the PDF with a text layer.
External GPU OCR server
An administrator can deploy jt-ocr-server to a GPU host from admin/ocr-langs → external GPU recognition server; jtdt then calls remote EasyOCR over HTTP, more than 10× faster than CPU. The related admin endpoints:
GET /admin/ocr-langs/deploy/install.sh # 下載安裝腳本
GET /admin/ocr-langs/deploy/uninstall.sh # 下載解除安裝腳本
GET /admin/api/ocr-langs/external/status # 讀取目前設定
POST /admin/api/ocr-langs/external/save # 儲存 URL / Token / Timeout / 啟用
POST /admin/api/ocr-langs/external/test # 測試連接
Once enabled, /tools/pdf-ocr/* and any tool that calls EasyOCR internally use the remote GPU, falling back to the local machine if the connection fails. jt-ocr-server's own endpoints:
GET /healthz # 不需 token,回 GPU/VRAM 資訊
GET /version # 需 Bearer token
POST /ocr # 需 Bearer token,multipart image + langs form
PDF annotation report
List every annotation in a PDF (page, type, author, content, coordinates, time).
POST /tools/pdf-annotations/api/pdf-annotations
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-annotations/api/pdf-annotations \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@reviewed.pdf" | jq
JSON response: full detail for each annotation.
Remove PDF annotations
Remove PDF annotations (filtered by type or author).
POST /tools/pdf-annotations-strip/api/pdf-annotations-strip
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
types | str | Remove only these types, as CSV (Highlight / Text / FreeText …); blank = remove everything | |
authors | str | Only these authors, as CSV; blank means all | |
mode | str | Processing mode |
curl -X POST http://localhost:8765/tools/pdf-annotations-strip/api/pdf-annotations-strip \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@reviewed.pdf" -F "types=Highlight,Text" \
--output clean.pdf
Response: the cleaned PDF.
Flatten PDF annotations
Burn annotations into the page content stream (the recipient cannot remove them; form fields stay fillable).
POST /tools/pdf-annotations-flatten/api/pdf-annotations-flatten
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-annotations-flatten/api/pdf-annotations-flatten \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@annotated.pdf" \
--output flattened.pdf
Response: the flattened PDF.
PDF hidden content scan
Scan a PDF for hidden content that could leak (metadata, covered text, layers, attachments …).
POST /tools/pdf-hidden-scan/api/pdf-hidden-scan
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ |
curl -X POST http://localhost:8765/tools/pdf-hidden-scan/api/pdf-hidden-scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" | jq
JSON response: what was found in each category.
Fill a PDF form
Detect form fields automatically and fill them from a company profile.
POST /tools/pdf-fill/api/pdf-fill
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | The PDF form to fill |
company_id | str | Company profile ID (created in the admin area) | |
font_id | str | Font ID for the filled text |
curl -X POST http://localhost:8765/tools/pdf-fill/api/pdf-fill \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@vendor_form.pdf" -F "company_id=acme" \
--output filled.pdf
Response: the filled PDF.
Stamp / sign a PDF
Overlay a stamp or signature image on a PDF.
POST /tools/pdf-stamp/api/pdf-stamp
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
stamp_image | file | ✓ | Stamp / signature image (a transparent PNG is best) |
x_mm | float | ✓ | X position of the bottom-left corner (mm) |
y_mm | float | ✓ | Y position of the bottom-left corner (mm) |
width_mm | float | ✓ | Width (mm) |
height_mm | float | ✓ | Height (mm) |
rotation_deg | float | Rotation, default 0 | |
page_mode | str | all (per page) / first / last, default all | |
pages_json | str | Specific pages: a JSON array of 0-based page numbers (such as [0,2,4]). When present it overrides page_mode; page numbers out of range are ignored | |
placements_json | str | Per-page positions (optional). A JSON array in which each object carries its own page number and coordinates → different pages can use different positions, and one page can hold several. When present it overrides x_mm / y_mm / width_mm / height_mm / rotation_deg / page_mode / pages_json; when absent the behaviour is exactly as before |
curl -X POST http://localhost:8765/tools/pdf-stamp/api/pdf-stamp \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@contract.pdf" -F "stamp_image=@chop.png" \
-F "x_mm=150" -F "y_mm=30" -F "width_mm=30" -F "height_mm=30" \
-F "pages_json=[0,2]" \
--output stamped.pdf
Response: the stamped PDF.
Per-page positions (placements): suited to multi-page contracts and renewals, where each page signs in a different spot and one page may need several signatures.
| placement fields | Type | Required | Description |
|---|---|---|---|
page | int | ✓ | 0-based page number; anything beyond the document is skipped |
x_mm / y_mm | float | ✓ | Position (mm) |
width_mm / height_mm | float | Size (mm), default 30×30 | |
rotation_deg | float | Rotation, default 0 | |
asset_id | str | Use an image from the shared assets instead (the uploaded stamp_image is used by default) |
# 第 1 頁蓋 2 處、第 3 頁蓋 1 處,第 2 頁不蓋
curl -X POST http://localhost:8765/tools/pdf-stamp/api/pdf-stamp \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@policy.pdf" -F "stamp_image=@sign.png" \
-F 'placements_json=[
{"page":0,"x_mm":40,"y_mm":250,"width_mm":20,"height_mm":20},
{"page":0,"x_mm":150,"y_mm":100,"width_mm":20,"height_mm":20},
{"page":2,"x_mm":100,"y_mm":150,"width_mm":25,"height_mm":25}]' \
--output stamped.pdf
PDF watermark
Add a text watermark.
POST /tools/pdf-watermark/api/pdf-watermark
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
text | str | ✓ | Watermark text |
opacity | float | Opacity 0; 1, default 0.15 | |
rotation_deg | float | Rotation, default 45 | |
mode | str | tile (tiled) / center (centred) | |
text_color | str | Text colour as hex | |
text_size_pt | float | Font size (pt) |
curl -X POST http://localhost:8765/tools/pdf-watermark/api/pdf-watermark \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" -F "text=機密" \
-F "opacity=0.12" -F "mode=tile" \
--output watermarked.pdf
Response: the watermarked PDF.
PDF editor
Burn text, images, shapes and masks into a PDF from an overlay JSON model (including genuine redaction).
POST /tools/pdf-editor/api/pdf-editor
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | The original PDF |
model | str (JSON) | ✓ | Overlay object model as a JSON string |
curl -X POST http://localhost:8765/tools/pdf-editor/api/pdf-editor \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" \
-F 'model={"version":1,"pages":[{"page":0,"objects":[{"id":"o1","type":"text","x":100,"y":200,"w":120,"h":20,"text":"已蓋章","font":"Noto Sans TC","size":14,"color":"#cc0000"}]}]}' \
--output edited.pdf
Response: the PDF with the overlay applied. The model format is documented in the web UI's pdf-editor.
Encrypt PDF
Add user and owner passwords and set permissions.
POST /tools/pdf-encrypt/api/pdf-encrypt
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | |
user_pw | str | Open password | |
owner_pw | str | Permissions password | |
algorithm | str | Encryption algorithm, e.g. AES-256 | |
allow_print | bool | Allow printing | |
allow_copy | bool | Allow copying content |
curl -X POST http://localhost:8765/tools/pdf-encrypt/api/pdf-encrypt \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@document.pdf" -F "user_pw=open123" \
-F "owner_pw=admin456" -F "algorithm=AES-256" -F "allow_print=true" \
--output encrypted.pdf
Response: the encrypted PDF.
Decrypt PDF
Remove PDF encryption with the password.
POST /tools/pdf-decrypt/api/pdf-decrypt
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | The encrypted PDF |
password | str | ✓ | Open password |
curl -X POST http://localhost:8765/tools/pdf-decrypt/api/pdf-decrypt \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@encrypted.pdf" -F "password=open123" \
--output decrypted.pdf
Response: the decrypted PDF.
Document redaction
Detect and mask personal data in Word and PDF documents (regex, with an optional LLM).
POST /tools/doc-deident/api/doc-deident
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | PDF / Word document |
types | str | PII types to detect as CSV (ID number / phone / email / address …); blank means the default set | |
mode | str | mask (mask, default) / redact (genuine deletion) / replace (replace with fake values) | |
replacements | str | replace only: a JSON object {"original": "the new value"}. Anything not listed is generated automatically | |
valid_checksum | str | replace only: 1 = generate fake values that pass their check digit (see below) |
curl -X POST http://localhost:8765/tools/doc-deident/api/doc-deident \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@contract.pdf" -F "mode=mask" \
--output deidentified.pdf
Response: the redacted file.
Replacement mode (mode=replace)
Replaces detected data with something that looks normal but is not real: useful for testing a system, reports that go outside, and teaching examples. The original is still genuinely deleted.
Values not listed in replacements are generated automatically, shaped correctly for each field type (ID number, tax ID, mobile number, email address, credit card, personal name, address, date …), and the same original always maps to the same fake value throughout one document.
valid_checksum is off by default, and the fake values it generates deliberately fails checksum validation, so it can never collide with a real person's data. Set to 1 makes ID numbers, tax IDs and credit cards carry a correct check digit (so a test system will not reject them; but a number that computes correctly could happen to belong to a real person). Email addresses use example.com, IP addresses use 192.0.2.x, MAC addresses use 00:00:5E, all of them ranges reserved for documentation, so either setting is safe.
curl -X POST http://localhost:8765/tools/doc-deident/api/doc-deident \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@contract.pdf" -F "mode=replace" \
-F 'replacements={"0912345678":"0955555555"}' \
-F "valid_checksum=1" \
--output replaced.pdf
Text redaction
Detect and mask personal data in plain text.
POST /tools/text-deident/api/text-deident
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
text | str | ✓ | The text to process |
mode | str | mask (default) / redact | |
types | array | Array of PII type IDs to detect; omit for the default set | |
custom_regex | str | Custom detection regex |
curl -X POST http://localhost:8765/tools/text-deident/api/text-deident \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"我的電話是 0912345678,身分證 A123456789","mode":"mask"}'
JSON response: the masked text plus the list of PII found.
Text compare
Compare two pieces of text.
POST /tools/text-diff/api/text-diff
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
text_a | str | ✓ | Old text |
text_b | str | ✓ | New text |
curl -X POST http://localhost:8765/tools/text-diff/api/text-diff \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text_a":"原始內容\n第二行","text_b":"修改內容\n第二行"}'
Response JSON: the type of each difference (equal / insert / delete / replace) and its content.
Document compare
Compare the contents of two documents (PDF / Word).
POST /tools/doc-diff/api/doc-diff
| Parameter | Type | Required | Description |
|---|---|---|---|
file_a | file | ✓ | Old document |
file_b | file | ✓ | New document |
curl -X POST http://localhost:8765/tools/doc-diff/api/doc-diff \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file_a=@v1.pdf" -F "file_b=@v2.pdf" | jq
Response JSON: the per-segment diff structure. Besides diff (the line-level text differences), each page also carries:
| Field | Description |
|---|---|
uid | the id of this comparison (at the top level of the response), used to fetch the page images |
pages[].marks.a / .b | the difference boxes on that page. Each is {tag, line, rects}, where tag is delete / insert / replace |
pages[].marks[].rects | [x, y, w, h],a 0~1 ratio (not pixels): multiply by the size of the page image you are showing |
pages[].size.a / .b | the page size in points, [width, height] |
When no text coordinates can be read (a scan, or text converted to outlines) marks is an empty array. That does not mean there are no differences — they are still in diff.
Page image
GET /tools/doc-diff/page-image/{uid}/{slot}/{page}
| Parameter | Description |
|---|---|
uid | the id returned by the comparison above |
slot | a (old version) or b (new version) |
page | 1-based |
Returns image/png (150 dpi). For office files this is the layout after conversion to PDF. An expired file returns 410; an invalid uid / slot / page always returns 404.
curl -X POST http://localhost:8765/tools/doc-diff/api/doc-diff \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file_a=@v1.pdf" -F "file_b=@v2.pdf" > diff.json
UID=$(jq -r .uid diff.json)
curl -H "Authorization: Bearer YOUR_TOKEN" \
"http://localhost:8765/tools/doc-diff/page-image/$UID/b/1" -o page1.png
List tools
Run a list of text through a pipeline of deduplication, sorting, counting and set operations.
POST /tools/text-list/api/text-list
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
text | str | ✓ | List text, one item per line |
ops | array | The processing pipeline; each element is {"op": "..."}, where op may be dedup / sort / count / exclude / lower / upper / title and others |
curl -X POST http://localhost:8765/tools/text-list/api/text-list \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"banana\napple\napple\ncherry","ops":[{"op":"dedup"},{"op":"sort"}]}'
Response JSON: {"lines": [...], "count": N, "original_count": M, ...}.
Document translation
Translates a whole office document into another language and returns a file in the same format with the same layout; only the text changes, nothing is re-laid-out. Supports .doc / .docx / .odt, .xls / .xlsx / .ods, .ppt / .pptx / .odp.
PDF is not accepted: a PDF has no paragraphs; its text is positioned fragments, and replacing them with translations of a different length is bound to break the layout. To translate a PDF, use “sentence translation” below.
POST /tools/doc-translate/api/doc-translate
Form (multipart):
| Field | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | Office document (the nine extensions above) |
target_lang | str | Target language, default zh-TW | |
source_lang | str | auto (default) / en / ja / ko … | |
domain | str | Domain hint (legal contract, medical report …) to sharpen the terminology |
curl -X POST http://localhost:8765/tools/doc-translate/api/doc-translate \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@contract.docx" \
-F "target_lang=en" \
-F "domain=法律合約" \
-o contract_translated.docx
The response is the translated file itself (Content-Disposition: attachment), in the same format as the upload.
This endpoint is synchronous: it responds only when the whole document is done, so a large file will hit the reverse proxy's timeout. The web interface uses the background-job version (
POST /tools/doc-translate/start, and after submitting use/api/jobs/{job_id}to check progress, thenGET /tools/doc-translate/download/{upload_id}to fetch the file). Requires an administrator to enable the LLM service (/admin/llm-settings). If it is not enabled it returns503。
Sentence translation
Sentence-by-sentence translation with a local LLM.
POST /tools/translate-doc/api/translate-doc
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
text | str | ✓ | Text to translate |
source_lang | str | auto (default) / en / zh / ja / ko ... | |
target_lang | str | Target language, default zh-TW | |
domain | str | Domain hint (sharpens specialist vocabulary) |
curl -X POST http://localhost:8765/tools/translate-doc/api/translate-doc \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text":"Hello world. This is a test.","source_lang":"auto","target_lang":"zh-TW"}'
JSON response:
{
"source_lang": "en",
"target_lang": "zh-TW",
"results": [
{"src": "Hello world.", "translated": "你好,世界。", "error": ""},
{"src": "This is a test.", "translated": "這是一個測試。", "error": ""}
]
}
Requires an administrator to enable the LLM service (
/admin/llm-settings). If it is not enabled it returns503。
Many sentences: the background-job version
The endpoint above is synchronous: it responds only when the whole document is done, so a large number of sentences will hit the reverse proxy's timeout. For a few hundred sentences or more, use the background job instead: you get a job id straight away, translation carries on the server, the caller can go offline, and progress and results can be fetched later.
POST /tools/translate-doc/start
GET /tools/translate-doc/job/{job_id}?start=0
start body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
sentences | list[str] | ✓ | An array of sentences you have already split (yourself, or with /extract-text) |
source_lang | str | auto (default) / en / zh … | |
target_lang | str | Target language, default zh-TW | |
domain | str | Domain hint | |
filename | str | Source filename for display (it appears in “My jobs”) |
JOB=$(curl -s -X POST http://localhost:8765/tools/translate-doc/start \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sentences":["Hello world.","This is a test."],"target_lang":"zh-TW"}' \
| jq -r .job_id)
curl -s "http://localhost:8765/tools/translate-doc/job/$JOB" \
-H "Authorization: Bearer YOUR_TOKEN" | jq
The response to a poll:
{
"status": "running",
"progress": 0.5,
"message": "翻譯中… 1 / 2 句",
"elapsed": 3.2,
"total": 2,
"start": 0,
"results": [
{"src": "Hello world.", "translated": "你好,世界。"},
{"src": "This is a test."}
],
"cancelled": false
}
status:pending/running/done/error/cancelled/interrupted- The source text is available as soon as it is submitted (items not yet translated simply have no
translatedfield),
so the caller can display results as they arrive.
start=Nreturns only the items after the Nth; with tens of thousands of sentences there is no need to fetch them all every time.- To cancel, use the shared job endpoint:
POST /api/jobs/{job_id}/cancel. - Ownership works as it does for every other job endpoint: anyone who is not the owner gets
404.
Company ID lookup (single)
Look up a company or agency name, address and industry by 8-digit tax ID.
POST /tools/vat-lookup/api/vat-lookup
Body (JSON): {"vat": "12345678"}
curl -X POST http://localhost:8765/tools/vat-lookup/api/vat-lookup \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vat":"12345678"}' | jq
Response JSON: company name, address, industry and so on. Not found returns 404.
Company ID lookup (path style)
The same, but as GET with a path parameter.
GET /api/vat-lookup/{vat}
curl http://localhost:8765/api/vat-lookup/12345678 \
-H "Authorization: Bearer YOUR_TOKEN" | jq
JSON response: the same as the single lookup.
Company ID lookup (batch)
Look up several tax IDs at once.
POST /tools/vat-lookup/api/vat-lookup/batch
Body (JSON): {"vats": ["12345678", "23456789"]}
curl -X POST http://localhost:8765/tools/vat-lookup/api/vat-lookup/batch \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vats":["12345678","23456789"]}' | jq
JSON response: an array of results.
e-Invoice scan
Decode the contents of an e-invoice QR code.
POST /tools/einvoice-scan/api/einvoice-scan
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | Image or PDF containing the invoice QR code |
curl -X POST http://localhost:8765/tools/einvoice-scan/api/einvoice-scan \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@invoice.jpg" | jq
JSON response: invoice number, date, amount, seller tax ID and so on.
e-Invoice backend status
Check whether the QR decoding backend (zbar) is available.
GET /tools/einvoice-scan/api/backend-status
curl http://localhost:8765/tools/einvoice-scan/api/backend-status \
-H "Authorization: Bearer YOUR_TOKEN" | jq
Response JSON: {"available": true/false, ...}.
Pre-submission check; your own company profile
Manage your own company entities for the pre-submission check (CRUD).
GET /tools/submission-check/api/self-entities
POST /tools/submission-check/api/self-entities
PUT /tools/submission-check/api/self-entities/{entity_id}
DELETE /tools/submission-check/api/self-entities/{entity_id}
POST / PUT parameters: name, tax_id, address, aliases, type, note.
# 列出
curl http://localhost:8765/tools/submission-check/api/self-entities \
-H "Authorization: Bearer YOUR_TOKEN" | jq
# 新增
curl -X POST http://localhost:8765/tools/submission-check/api/self-entities \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "name=Acme 股份有限公司" -F "tax_id=12345678" \
-F "address=台北市..." -F "type=company"
# 更新
curl -X PUT http://localhost:8765/tools/submission-check/api/self-entities/abc123 \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "name=Acme 國際" -F "tax_id=12345678"
# 刪除
curl -X DELETE http://localhost:8765/tools/submission-check/api/self-entities/abc123 \
-H "Authorization: Bearer YOUR_TOKEN"
JSON response: the entity list or the result of the operation.
Travel receipts
Parses a batch of Taiwan Railway, THSR or Uber ride receipt PDFs and returns structured JSON. Parsing only — nothing is written to the user's list.
POST /tools/transit-proof/api/transit-proof
| Parameter | Type | Required | Description |
|---|---|---|---|
files | file[] | ✓ | Travel receipt PDFs, up to 200 at a time |
curl -X POST http://localhost:8765/tools/transit-proof/api/transit-proof \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "files=@proof1.pdf" -F "files=@proof2.pdf" | jq
{
"ok": true,
"count": 1,
"entries": [{"date": "...", "from": "...", "to": "...", "amount": 0}],
"failed": [{"file": "other.pdf",
"error": "無法辨識為乘車證明(格式不符或版面不支援)"}]
}
A file that cannot be recognised does not fail the whole batch; successes go into entries and each failure is listed in failed (the HTTP status is still 200). To tell whether anything was missed, check whether failed is empty rather than going by the HTTP status. More than 200 files returns 400; no files at all returns 422.
Long or batch operations go through the job queue. The flow:
- Call the tool's submit endpoint → receive
{"job_id": "..."} - Poll
GET /api/jobs/{job_id}untilstatus == "completed" - Download the result with
GET /api/jobs/{job_id}/download(a single PDF, or a ZIP for several files)
LLM verification (pdf-fill)
POST /api/llm-review
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | PDF (fields already filled, ready for verification) |
template_id | str | ✓ | Template ID (the layout remembered in the admin area) |
rounds | int | Number of review rounds; defaults to the admin setting |
curl -X POST http://localhost:8765/api/llm-review \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@filled.pdf" -F "template_id=vendor_form_v3" \
| jq
# → {"job_id": "..."}
Response: {"job_id": "..."}.
Check job status
GET /api/jobs/{job_id}
curl http://localhost:8765/api/jobs/abc123 \
-H "Authorization: Bearer YOUR_TOKEN" | jq
Response:
{
"job_id": "abc123...",
"status": "running",
"progress": 0.65,
"message": "校驗第 5 / 12 欄位",
"error": null,
"tool": "pdf-fill-llm"
}
status:pending / running / completed / failed。
Download a job result
GET /api/jobs/{job_id}/download
GET /api/jobs/{job_id}/download/{filename} # 同一份結果,只是讓瀏覽器存成這個檔名
curl http://localhost:8765/api/jobs/abc123/download \
-H "Authorization: Bearer YOUR_TOKEN" \
--output result.pdf
Response: the result file (PDF / ZIP). If it is not finished (status != completed) the call returns 409.
Download a job result as PNG
GET /api/jobs/{job_id}/download-png
curl http://localhost:8765/api/jobs/abc123/download-png \
-H "Authorization: Bearer YOUR_TOKEN" \
--output result.zip
Response: the job's PDF result rendered as PNG (several pages or files come back as a ZIP).
Requires an administrator sign-in or an admin-role token.
List assets
List every stamp, signature, logo and watermark asset.
GET /admin/api/assets
curl http://localhost:8765/admin/api/assets \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
Read / update LLM settings
GET /admin/api/llm/settings
POST /admin/api/llm/settings
# 讀取
curl http://localhost:8765/admin/api/llm/settings \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
# 更新
curl -X POST http://localhost:8765/admin/api/llm/settings \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"base_url":"http://localhost:11434","model":"gemma4:26b"}'
Test the LLM connection
POST /admin/api/llm/test-connection
| Parameter | Type | Required | Description |
|---|---|---|---|
base_url | str | The address to test. Omit it and the saved settings are tested instead | |
api_key | str | Same; omit it and the saved key is used | |
timeout_seconds | num | Capped at 30 seconds, so the admin page cannot hang |
A JSON body. The admin page’s "Test connection" button sends the settings that are not saved yet; from the command line, when you only want to know whether the current settings work, the whole body can be omitted.
# 測目前存檔的設定
curl -X POST http://localhost:8765/admin/api/llm/test-connection \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
# → {"ok": true, "latency_ms": 42, "error": null, "models": [...]}
# 測一組還沒存的設定
curl -X POST http://localhost:8765/admin/api/llm/test-connection \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"base_url": "http://10.0.0.5:11434/v1"}' | jq
When the connection fails it still returns 200, with ok: false and an error message; being unable to reach the server is not a failure of this request itself.
Fetch the LLM model list
GET /admin/api/llm/models
curl http://localhost:8765/admin/api/llm/models \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
System dependency status
GET /admin/api/sys-deps
curl http://localhost:8765/admin/api/sys-deps \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
Company logo status
GET /admin/api/branding
curl http://localhost:8765/admin/api/branding \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
Settings export list
GET /admin/api/settings-export/categories
curl http://localhost:8765/admin/api/settings-export/categories \
-H "Authorization: Bearer ADMIN_TOKEN" | jq
Token management
POST /admin/api/tokens/create # 核發新 token
POST /admin/api/tokens/revoke # 撤銷 token
POST /admin/api/tokens/enforce # 開關 enforce 模式
# 核發
curl -X POST http://localhost:8765/admin/api/tokens/create \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label":"gitlab-ci"}' | jq
# 開啟 enforce
curl -X POST http://localhost:8765/admin/api/tokens/enforce \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enforce":true}'
Signed-URL file fetch (not meant to be called by hand)
Some external services only accept a URL — the file is not uploaded to them, they fetch it themselves from an address we provide. In that case we hand over a short-lived signed URL rather than an API token:
GET /api/speech/audio/{file_id}?exp=<到期的 unix 秒數>&sig=<簽章>
- The URL itself is the authentication, with no
Authorizationheader. The signature binds the file id together with the expiry,
so extending exp invalidates the signature.
- Anything that fails to verify returns 404 — a malformed id, a wrong signature, an expired URL and a missing file all
look identical from outside (a 403 would tell the caller “that id exists”).
- The URL is produced server-side, and there is no public endpoint that hands out a signature —
it is listed here so that this document misses no endpoint, not for you to call.
If a fetch returns 404, the usual cause is an expired URL, not a missing file.
XHR endpoints used by the admin UI itself
These are endpoints the admin pages call themselves, not a stable public interface: their parameters and responses change along with the pages, so no compatibility is promised. They are listed so that this document covers every endpoint; for automation, prefer the APIs above, which have a defined contract.
| Endpoint | Method | Admin page |
|---|---|---|
/admin/api/check-latest-version | POST | System status: check for a newer release |
/admin/api/upload-limit/probe | POST | System status: measure the reverse proxy's upload limit |
/admin/api/ocr-langs/set-engine | POST | OCR language packs: switch the default engine |
/admin/api/ocr-langs/set-quality | POST | OCR language packs: switch recognition quality |
/admin/api/ocr-langs/switch-active | POST | OCR language packs: switch the active language |
/admin/jobs/api/list | GET | Job queue: current jobs |
/admin/jobs/api/history | GET | Job queue: history |
/admin/jobs/api/cancel/{job_id} | POST | Job queue: cancel one job |
/admin/jobs/api/pause | POST | Job queue: pause or resume dispatching |
/admin/jobs/api/concurrency | POST | Job queue: maximum concurrent jobs |
/admin/jobs/api/priority-users | GET / POST | Job queue: priority dispatch list (the order is the priority) |
/admin/jobs/api/user-search | GET | Job queue: user search box for picking priority users |
GitLab CI / GitHub Actions: convert Word documents to PDF automatically
# .gitlab-ci.yml
convert-docs:
script:
- |
for f in docs/*.docx; do
curl -fsSL -X POST "http://jtdt.internal:8765/api/convert-to-pdf" \
-H "Authorization: Bearer $JTDT_TOKEN" \
-F "file=@$f" \
--output "build/$(basename "$f " .docx).pdf"
done
artifacts:
paths: [build/]
Python client: clear PDF annotations in bulk
import requests
from pathlib import Path
API = "http://localhost:8765"
TOKEN = "YOUR_64_HEX_TOKEN"
H = {"Authorization": f "Bearer {TOKEN}"}
for pdf in Path("incoming/").glob("*.pdf"):
with pdf.open("rb") as f:
r = requests.post(
f "{API}/tools/pdf-annotations-strip/api/pdf-annotations-strip",
headers=H,
files={"file": (pdf.name, f, "application/pdf")},
)
r.raise_for_status()
(Path("clean/") / pdf.name).write_bytes(r.content)
print(f "OK {pdf.name}")
Shell: watch for a job to finish and download it
#!/bin/bash
TOKEN="YOUR_TOKEN"
API="http://localhost:8765"
JOB=$(curl -fsSL -X POST "$API/api/llm-review" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@filled.pdf" -F "template_id=vendor_form_v3" \
| jq -r .job_id)
echo "Job: $JOB"
while :; do
S=$(curl -fsSL "$API/api/jobs/$JOB" -H "Authorization: Bearer $TOKEN")
STATE=$(echo "$S" | jq -r .status)
PROG=$(echo "$S" | jq -r .progress)
echo " $STATE $PROG"
[ "$STATE" = "completed" ] && break
[ "$STATE" = "failed" ] && { echo "Failed"; exit 1; }
sleep 2
done
curl -fsSL "$API/api/jobs/$JOB/download" \
-H "Authorization: Bearer $TOKEN" \
--output reviewed.pdf
echo "Saved: reviewed.pdf"
Node.js: sentence translation
const r = await fetch(
'http://localhost:8765/tools/translate-doc/api/translate-doc',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
},
body: JSON.stringify({
text: 'Hello world. This is a test.',
source_lang: 'auto',
target_lang: 'zh-TW',
}),
}
);
const j = await r.json();
console.log(j.results);
// [{src: 'Hello world.', translated: '你好,世界。', error: ''}, ...]
13. Managing tokens from the CLI
# 列出
sudo jtdt auth show
# 直接讀檔可看(unhash 不可逆):
sudo cat /var/lib/jt-doc-tools/data/api_tokens.json
# 撤銷(CLI 沒提供撤銷,必要時直接清掉檔案後重啟服務):
sudo systemctl stop jt-doc-tools
sudo rm /var/lib/jt-doc-tools/data/api_tokens.json
sudo systemctl start jt-doc-tools
# 重啟後 admin UI 重新核發
14. Rate limits and upload size
There is no built-in rate limit at present. Add one at the reverse proxy (nginx / Caddy) when you deploy:
client_max_body_size 100M(required, otherwise large PDFs are rejected)proxy_read_timeout 900s+proxy_send_timeout 900s(required; a single LLM inference often takes 5-15 minutes, and the 60s default is guaranteed to give a 504)proxy_buffering off(friendly to LLM streaming)- With several layers of nginx (a self-hosted LLM proxy plus jt-doc-tools) every layer needs it; one layer left at the default breaks the whole chain
- If it needs to be exposed publicly, add
limit_req_zoneagainst abuse
See the "reverse proxy" section and the "504 Gateway Timeout troubleshooting" section of OPS.md.
14b. User workspace (session authentication, not a Bearer API)
"My workspace" is a web feature tied to the signed-in session (cookie authentication) and is not part of the public Bearer API, because each file is isolated by account. An administrator must enable it in "workspace settings"; while it is disabled every endpoint below returns 404.
| Endpoint | Method | Description |
|---|---|---|
/workspace | GET | The “My workspace” page |
/workspace/api/list | GET | List your own files (filter with ?accept=pdf,png) plus the quota and retention period |
/workspace/api/count | GET | File count (for the sidebar badge) |
/workspace/save | POST | Save a file: job_id (the server copies the job result) or file (upload the bytes directly); PDF / PNG only |
/workspace/file/{file_id} | GET | Fetch a file (?dl=1 to download) |
/workspace/thumb/{file_id} | GET | Thumbnail (first PDF page rendered / the PNG itself) |
/workspace/delete | POST | Delete (file_id) |
/workspace/rename | POST | Rename (file_id, name) |
The quota, per-file limit, retention period and the on/off switch are set by an administrator at /admin/workspace (site-wide, with no individual exceptions).
14c. User notifications and inbox (session auth, not the Bearer API)
Same nature as the workspace in §14b: bound to the login session (cookie), not part of the public Bearer API, because the contents are isolated per signed-in user. Requests without a session get 401 / 302.
| Endpoint | Method | Description |
|---|---|---|
/api/my/notify | GET | Read your own notification settings (whether to notify on job completion, and through which channels) |
/api/my/notify | POST | Save your own notification settings |
/api/my/inbox | GET | In-app notification list (unread first) |
/api/my/inbox/seen | POST | Mark as read ( id or all=1 ) |
These four are deliberately left out of the Bearer API: they return data belonging to the signed-in user, so calling them with a token makes little sense (a token is bound to a user, but these endpoints are designed for the browser).
15. Change history
The API follows SemVer: a minor release (1.4.x → 1.5.x) is guaranteed backwards compatible; only a major release (1.x → 2.x) may break. Adding an endpoint is not a breaking change.
The full change log is in CHANGELOG.md.