diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts
index 7d661aa6..1864ac2d 100644
--- a/apps/docs/.vitepress/config.mts
+++ b/apps/docs/.vitepress/config.mts
@@ -149,6 +149,9 @@ export default defineConfig({
{ text: "Sharpening", link: "/tools/sharpening" },
{ text: "Replace & Invert Color", link: "/tools/replace-color" },
{ text: "Color Blindness Simulation", link: "/tools/color-blindness" },
+ { text: "Duotone", link: "/tools/duotone" },
+ { text: "Pixelate", link: "/tools/pixelate" },
+ { text: "Vignette", link: "/tools/vignette" },
],
},
{
@@ -172,6 +175,9 @@ export default defineConfig({
{ text: "HTML to Image", link: "/tools/html-to-image" },
{ text: "Barcode Reader", link: "/tools/barcode-read" },
{ text: "Image to Base64", link: "/tools/image-to-base64" },
+ { text: "Histogram", link: "/tools/histogram" },
+ { text: "LQIP Placeholder", link: "/tools/lqip-placeholder" },
+ { text: "Barcode Generator", link: "/tools/barcode-generate" },
],
},
{
@@ -182,6 +188,9 @@ export default defineConfig({
{ text: "Image Splitting", link: "/tools/split" },
{ text: "Border & Frame", link: "/tools/border" },
{ text: "Beautify Screenshot", link: "/tools/beautify" },
+ { text: "Circle Crop", link: "/tools/circle-crop" },
+ { text: "Image Pad", link: "/tools/image-pad" },
+ { text: "Sprite Sheet", link: "/tools/sprite-sheet" },
],
},
{
@@ -190,7 +199,7 @@ export default defineConfig({
{ text: "SVG to Raster", link: "/tools/svg-to-raster" },
{ text: "Image to SVG", link: "/tools/vectorize" },
{ text: "GIF Tools", link: "/tools/gif-tools" },
- { text: "PDF to Image", link: "/tools/pdf-to-image" },
+ { text: "GIF/WebP Converter", link: "/tools/gif-webp" },
],
},
{
@@ -212,15 +221,121 @@ export default defineConfig({
{ text: "Content-Aware Resize", link: "/tools/content-aware-resize" },
{ text: "AI Canvas Expand", link: "/tools/ai-canvas-expand" },
{ text: "PNG Transparency Fixer", link: "/tools/transparency-fixer" },
+ { text: "Background Replace", link: "/tools/background-replace" },
+ { text: "Blur Background", link: "/tools/blur-background" },
],
},
{
- text: "Video, Audio, Document & Data",
+ text: "Video",
items: [
- { text: "Video tools", link: "/api/rest#video-tools" },
- { text: "Audio tools", link: "/api/rest#audio-tools" },
- { text: "Document tools", link: "/api/rest#document-tools" },
- { text: "Data tools", link: "/api/rest#data-tools" },
+ { text: "Convert Video", link: "/tools/convert-video" },
+ { text: "Compress Video", link: "/tools/compress-video" },
+ { text: "Trim Video", link: "/tools/trim-video" },
+ { text: "Mute Video", link: "/tools/mute-video" },
+ { text: "Video to GIF", link: "/tools/video-to-gif" },
+ { text: "Resize Video", link: "/tools/resize-video" },
+ { text: "Crop Video", link: "/tools/crop-video" },
+ { text: "Rotate Video", link: "/tools/rotate-video" },
+ { text: "Change FPS", link: "/tools/change-fps" },
+ { text: "Video Color", link: "/tools/video-color" },
+ { text: "Video Speed", link: "/tools/video-speed" },
+ { text: "Reverse Video", link: "/tools/reverse-video" },
+ { text: "Normalize Audio", link: "/tools/video-loudnorm" },
+ { text: "Aspect Pad", link: "/tools/aspect-pad" },
+ { text: "Blur Pad", link: "/tools/blur-pad" },
+ { text: "Watermark Video", link: "/tools/watermark-video" },
+ { text: "Stabilize Video", link: "/tools/stabilize-video" },
+ { text: "GIF to Video", link: "/tools/gif-to-video" },
+ { text: "Video to WebP", link: "/tools/video-to-webp" },
+ { text: "Video to Frames", link: "/tools/video-to-frames" },
+ { text: "Merge Videos", link: "/tools/merge-videos" },
+ { text: "Replace Audio", link: "/tools/replace-audio" },
+ { text: "Burn Subtitles", link: "/tools/burn-subtitles" },
+ { text: "Embed Subtitles", link: "/tools/embed-subtitles" },
+ { text: "Extract Subtitles", link: "/tools/extract-subtitles" },
+ { text: "Images to Video", link: "/tools/images-to-video" },
+ { text: "Clean Video Metadata", link: "/tools/video-metadata" },
+ { text: "Auto Subtitles", link: "/tools/auto-subtitles" },
+ { text: "Extract Audio", link: "/tools/extract-audio" },
+ ],
+ },
+ {
+ text: "Audio",
+ items: [
+ { text: "Convert Audio", link: "/tools/convert-audio" },
+ { text: "Trim Audio", link: "/tools/trim-audio" },
+ { text: "Volume Adjust", link: "/tools/volume-adjust" },
+ { text: "Normalize Audio", link: "/tools/normalize-audio" },
+ { text: "Fade Audio", link: "/tools/fade-audio" },
+ { text: "Reverse Audio", link: "/tools/reverse-audio" },
+ { text: "Audio Speed", link: "/tools/audio-speed" },
+ { text: "Pitch Shift", link: "/tools/pitch-shift" },
+ { text: "Audio Channels", link: "/tools/audio-channels" },
+ { text: "Silence Removal", link: "/tools/silence-removal" },
+ { text: "Noise Reduction", link: "/tools/noise-reduction" },
+ { text: "Merge Audio", link: "/tools/merge-audio" },
+ { text: "Split Audio", link: "/tools/split-audio" },
+ { text: "Ringtone Maker", link: "/tools/ringtone-maker" },
+ { text: "Waveform Image", link: "/tools/waveform-image" },
+ { text: "Audio Metadata", link: "/tools/audio-metadata" },
+ { text: "Transcribe Audio", link: "/tools/transcribe-audio" },
+ ],
+ },
+ {
+ text: "PDF & Documents",
+ items: [
+ { text: "PDF to Image", link: "/tools/pdf-to-image" },
+ { text: "Merge PDFs", link: "/tools/merge-pdf" },
+ { text: "Split PDF", link: "/tools/split-pdf" },
+ { text: "Compress PDF", link: "/tools/compress-pdf" },
+ { text: "Rotate PDF", link: "/tools/rotate-pdf" },
+ { text: "Convert Document", link: "/tools/convert-document" },
+ { text: "Convert Presentation", link: "/tools/convert-presentation" },
+ { text: "Convert Spreadsheet", link: "/tools/convert-spreadsheet" },
+ { text: "Excel to PDF", link: "/tools/excel-to-pdf" },
+ { text: "Word to PDF", link: "/tools/word-to-pdf" },
+ { text: "Extract Pages", link: "/tools/extract-pages" },
+ { text: "Remove Pages", link: "/tools/remove-pages" },
+ { text: "Organize PDF", link: "/tools/organize-pdf" },
+ { text: "Protect PDF", link: "/tools/protect-pdf" },
+ { text: "Unlock PDF", link: "/tools/unlock-pdf" },
+ { text: "Repair PDF", link: "/tools/repair-pdf" },
+ { text: "Web-Optimize PDF", link: "/tools/linearize-pdf" },
+ { text: "Grayscale PDF", link: "/tools/grayscale-pdf" },
+ { text: "PDF/A Convert", link: "/tools/pdfa-convert" },
+ { text: "Crop PDF", link: "/tools/crop-pdf" },
+ { text: "N-up PDF", link: "/tools/nup-pdf" },
+ { text: "Booklet PDF", link: "/tools/booklet-pdf" },
+ { text: "Watermark PDF", link: "/tools/watermark-pdf" },
+ { text: "PDF Page Numbers", link: "/tools/pdf-page-numbers" },
+ { text: "Flatten PDF", link: "/tools/flatten-pdf" },
+ { text: "Redact PDF", link: "/tools/redact-pdf" },
+ { text: "PDF to Text", link: "/tools/pdf-to-text" },
+ { text: "PDF to Word", link: "/tools/pdf-to-word" },
+ { text: "PDF Metadata", link: "/tools/pdf-metadata" },
+ { text: "PowerPoint to PDF", link: "/tools/powerpoint-to-pdf" },
+ { text: "HTML to PDF", link: "/tools/html-to-pdf" },
+ { text: "Markdown to Word", link: "/tools/markdown-to-docx" },
+ { text: "Markdown to HTML", link: "/tools/markdown-to-html" },
+ { text: "Markdown to PDF", link: "/tools/markdown-to-pdf" },
+ { text: "Convert EPUB", link: "/tools/epub-convert" },
+ { text: "Convert to EPUB", link: "/tools/to-epub" },
+ { text: "PDF OCR", link: "/tools/ocr-pdf" },
+ ],
+ },
+ {
+ text: "Data",
+ items: [
+ { text: "Chart Maker", link: "/tools/chart-maker" },
+ { text: "CSV to Excel", link: "/tools/csv-excel" },
+ { text: "CSV to JSON", link: "/tools/csv-json" },
+ { text: "JSON to XML", link: "/tools/json-xml" },
+ { text: "Split CSV", link: "/tools/split-csv" },
+ { text: "Merge CSVs", link: "/tools/merge-csvs" },
+ { text: "YAML / JSON", link: "/tools/yaml-json" },
+ { text: "XML to CSV", link: "/tools/xml-to-csv" },
+ { text: "Create ZIP", link: "/tools/create-zip" },
+ { text: "Extract ZIP", link: "/tools/extract-zip" },
],
},
],
diff --git a/apps/docs/tools/aspect-pad.md b/apps/docs/tools/aspect-pad.md
new file mode 100644
index 00000000..8cdb308d
--- /dev/null
+++ b/apps/docs/tools/aspect-pad.md
@@ -0,0 +1,46 @@
+---
+description: Add solid-color bars to fit a target aspect ratio.
+---
+
+# Aspect Pad
+
+Add solid-color letterbox or pillarbox bars to fit a video into a target aspect ratio without cropping.
+
+## API Endpoint
+
+`POST /api/v1/tools/aspect-pad`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| target | string | No | `"9:16"` | Target aspect ratio: `16:9`, `9:16`, `1:1`, `4:3`, `3:4` |
+| color | string | No | `"#000000"` | Hex color for the padding bars (e.g. `"#000000"` for black) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/aspect-pad \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"target": "1:1", "color": "#ffffff"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 13200000
+}
+```
+
+## Notes
+
+- If the video already matches the target aspect ratio, the file is returned unchanged.
+- Use `9:16` for vertical/portrait social media formats (TikTok, Reels, Shorts).
+- For blurred padding instead of solid color, use the Blur Pad tool.
diff --git a/apps/docs/tools/audio-channels.md b/apps/docs/tools/audio-channels.md
new file mode 100644
index 00000000..95e72268
--- /dev/null
+++ b/apps/docs/tools/audio-channels.md
@@ -0,0 +1,46 @@
+---
+description: Convert between mono and stereo or swap left and right channels.
+---
+
+# Audio Channels
+
+Convert audio between mono and stereo layouts, or swap the left and right channels of a stereo file.
+
+## API Endpoint
+
+`POST /api/v1/tools/audio-channels`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| mode | string | Yes | - | Channel operation: `stereo-to-mono`, `mono-to-stereo`, `swap` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/audio-channels \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"mode": "stereo-to-mono"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 2300000
+}
+```
+
+## Notes
+
+- `stereo-to-mono` mixes both channels into a single mono track.
+- `mono-to-stereo` duplicates the mono channel to both left and right.
+- `swap` exchanges the left and right channels of a stereo file.
+- Output format matches the input format.
diff --git a/apps/docs/tools/audio-metadata.md b/apps/docs/tools/audio-metadata.md
new file mode 100644
index 00000000..c992131f
--- /dev/null
+++ b/apps/docs/tools/audio-metadata.md
@@ -0,0 +1,70 @@
+---
+description: View, edit, or strip audio metadata tags (ID3).
+---
+
+# Audio Metadata
+
+View, edit, or strip audio metadata tags such as title, artist, and album (ID3 and similar tag formats).
+
+## API Endpoint
+
+`POST /api/v1/tools/audio-metadata`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| strip | boolean | No | `false` | Remove all existing metadata tags |
+| title | string | No | - | Set the title tag (max 500 characters) |
+| artist | string | No | - | Set the artist tag (max 500 characters) |
+| album | string | No | - | Set the album tag (max 500 characters) |
+
+## Example Request
+
+Edit metadata tags:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/audio-metadata \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"title": "My Song", "artist": "Artist Name", "album": "Album Name"}'
+```
+
+Strip all metadata:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/audio-metadata \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"strip": true}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4480000,
+ "metadata": {
+ "container": "mp3",
+ "durationS": 245.3,
+ "bitrateKbps": 192,
+ "tags": {
+ "title": "My Song",
+ "artist": "Artist Name",
+ "album": "Album Name"
+ }
+ }
+}
+```
+
+## Notes
+
+- The response includes a `metadata` object with container format, duration, bitrate, and current tags.
+- When `strip` is `true`, all tag fields are ignored and every existing tag is removed.
+- Only the tags you provide are updated; unspecified tags remain unchanged.
+- Output format matches the input format.
diff --git a/apps/docs/tools/audio-speed.md b/apps/docs/tools/audio-speed.md
new file mode 100644
index 00000000..a0dbe4ef
--- /dev/null
+++ b/apps/docs/tools/audio-speed.md
@@ -0,0 +1,45 @@
+---
+description: Speed up or slow down audio playback with a multiplier.
+---
+
+# Audio Speed
+
+Speed up or slow down audio playback by applying a speed multiplier.
+
+## API Endpoint
+
+`POST /api/v1/tools/audio-speed`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| factor | number | No | `1.5` | Speed multiplier (0.25 to 4). Values below 1 slow down; above 1 speed up. |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/audio-speed \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"factor": 2}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 2250000
+}
+```
+
+## Notes
+
+- A factor of `0.25` plays at quarter speed (4x longer). A factor of `4` plays at quadruple speed (4x shorter).
+- Pitch changes proportionally with speed. Use pitch-shift if you need to adjust pitch independently.
+- Output format matches the input format.
diff --git a/apps/docs/tools/auto-subtitles.md b/apps/docs/tools/auto-subtitles.md
new file mode 100644
index 00000000..3eedc4b5
--- /dev/null
+++ b/apps/docs/tools/auto-subtitles.md
@@ -0,0 +1,45 @@
+---
+description: Generate subtitle files from video audio tracks using AI.
+---
+
+# Auto Subtitles
+
+Generate subtitle files from a video's audio track using AI-powered speech recognition (faster-whisper). Supports auto-detection and 10 explicit languages.
+
+## API Endpoint
+
+`POST /api/v1/tools/auto-subtitles`
+
+Accepts multipart form data with a video file and a JSON `settings` field. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| language | string | No | `"auto"` | Speech language: `auto`, `en`, `de`, `fr`, `es`, `zh`, `ja`, `ko`, `id`, `th`, `vi` |
+| format | string | No | `"srt"` | Output subtitle format: `srt`, `vtt` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/auto-subtitles \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"language": "en", "format": "srt"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- This is an AI tool that requires the **transcription** feature bundle to be installed. If the bundle is not installed, the API returns `501 Feature Not Installed` with instructions to install it via the admin UI.
+- The `auto` language option uses whisper's built-in language detection. Specifying the language explicitly improves accuracy and speed.
+- SRT is the most widely supported subtitle format. VTT (WebVTT) is the standard for web video players.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/background-replace.md b/apps/docs/tools/background-replace.md
new file mode 100644
index 00000000..55ee767f
--- /dev/null
+++ b/apps/docs/tools/background-replace.md
@@ -0,0 +1,52 @@
+---
+description: Replace image background with a solid color or gradient using AI.
+---
+
+# Background Replace
+
+Replace the background of an image with a solid color or gradient. The AI model detects the subject, removes the original background, and composites the subject onto your chosen background.
+
+## API Endpoint
+
+`POST /api/v1/tools/background-replace`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| backgroundType | string | No | `"color"` | Background mode: `color` or `gradient` |
+| color | string | No | `"#ffffff"` | Background hex color (when backgroundType is `color`) |
+| gradientColor1 | string | No | - | First gradient hex color |
+| gradientColor2 | string | No | - | Second gradient hex color |
+| gradientAngle | integer | No | `180` | Gradient angle in degrees (0-360) |
+| feather | integer | No | `0` | Edge feathering radius (0-20) |
+| format | string | No | `"png"` | Output format: `png` or `webp` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/background-replace \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"backgroundType": "color", "color": "#2563eb", "feather": 2}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+Track progress via SSE at `GET /api/v1/jobs/{jobId}/progress`. When the job completes, the SSE stream emits a `completed` event with the download URL.
+
+## Notes
+
+- This is an AI-powered tool that returns `202 Accepted` and processes asynchronously. Connect to the SSE endpoint to receive progress updates and the final result.
+- Requires the **background-removal** feature bundle to be installed. Returns `501` if the bundle is not available.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
+- Output defaults to PNG to preserve transparency around the subject.
diff --git a/apps/docs/tools/barcode-generate.md b/apps/docs/tools/barcode-generate.md
new file mode 100644
index 00000000..13b4b88a
--- /dev/null
+++ b/apps/docs/tools/barcode-generate.md
@@ -0,0 +1,49 @@
+---
+description: Generate barcodes in Code 128, EAN-13, UPC-A, Code 39, ITF-14, and Data Matrix formats.
+---
+
+# Barcode Generator
+
+Generate barcode images from text input. Supports Code 128, EAN-13, UPC-A, Code 39, ITF-14, and Data Matrix formats.
+
+## API Endpoint
+
+`POST /api/v1/tools/barcode-generate`
+
+Accepts an `application/json` body (not multipart). The barcode is generated from the provided text, not from an uploaded file.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| text | string | Yes | - | Text to encode in the barcode (1-256 characters) |
+| type | string | No | `"code128"` | Barcode format: `code128`, `ean13`, `upca`, `code39`, `itf14`, `datamatrix` |
+| scale | integer | No | `3` | Image scale factor (1-8) |
+| includeText | boolean | No | `true` | Whether to render the text below the barcode |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/barcode-generate \
+ -H "Authorization: Bearer si_your-api-key" \
+ -H "Content-Type: application/json" \
+ -d '{"text": "5901234123457", "type": "ean13", "scale": 4}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/barcode.png",
+ "originalSize": 0,
+ "processedSize": 4520
+}
+```
+
+## Notes
+
+- Unlike most tools, this endpoint accepts a JSON body, not multipart form data, since barcodes are generated from text rather than an uploaded file.
+- EAN-13 requires exactly 12 or 13 digits. UPC-A requires exactly 11 or 12 digits. If a check digit is omitted, it is calculated automatically.
+- Code 128 is the most flexible format and supports the full ASCII character set.
+- Data Matrix produces a 2D barcode suitable for encoding longer strings in a compact square.
diff --git a/apps/docs/tools/blur-background.md b/apps/docs/tools/blur-background.md
new file mode 100644
index 00000000..a95e48cb
--- /dev/null
+++ b/apps/docs/tools/blur-background.md
@@ -0,0 +1,48 @@
+---
+description: Blur the background while keeping the subject sharp using AI.
+---
+
+# Blur Background
+
+Blur the background of an image while keeping the subject sharp. The AI model isolates the subject, applies a blur to the original background, and composites the sharp subject on top.
+
+## API Endpoint
+
+`POST /api/v1/tools/blur-background`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| intensity | integer | No | `50` | Blur intensity (1-100) |
+| feather | integer | No | `0` | Edge feathering radius (0-20) |
+| format | string | No | `"png"` | Output format: `png` or `webp` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/blur-background \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"intensity": 75, "feather": 3}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+Track progress via SSE at `GET /api/v1/jobs/{jobId}/progress`. When the job completes, the SSE stream emits a `completed` event with the download URL.
+
+## Notes
+
+- This is an AI-powered tool that returns `202 Accepted` and processes asynchronously. Connect to the SSE endpoint to receive progress updates and the final result.
+- Requires the **background-removal** feature bundle to be installed. Returns `501` if the bundle is not available.
+- Higher intensity values produce a stronger blur effect. Values above 80 create a pronounced bokeh-like separation.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/blur-pad.md b/apps/docs/tools/blur-pad.md
new file mode 100644
index 00000000..21c0ae0f
--- /dev/null
+++ b/apps/docs/tools/blur-pad.md
@@ -0,0 +1,46 @@
+---
+description: Fill bars with a blurred copy of the video.
+---
+
+# Blur Pad
+
+Fit a video into a target aspect ratio by filling the padding area with a blurred, scaled copy of the video instead of solid-color bars.
+
+## API Endpoint
+
+`POST /api/v1/tools/blur-pad`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| target | string | No | `"16:9"` | Target aspect ratio: `16:9`, `9:16`, `1:1`, `4:3`, `3:4` |
+| blur | number | No | `20` | Gaussian blur sigma for the background (2-50) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/blur-pad \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"target": "16:9", "blur": 30}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 14100000
+}
+```
+
+## Notes
+
+- Higher blur values produce a softer, more abstract background. Lower values keep more detail visible.
+- If the video already matches the target aspect ratio, the file is returned unchanged.
+- For solid-color padding, use the Aspect Pad tool instead.
diff --git a/apps/docs/tools/booklet-pdf.md b/apps/docs/tools/booklet-pdf.md
new file mode 100644
index 00000000..a9901be7
--- /dev/null
+++ b/apps/docs/tools/booklet-pdf.md
@@ -0,0 +1,45 @@
+---
+description: Arrange PDF pages for folding into a booklet.
+---
+
+# Booklet PDF
+
+Impose pages for duplex printing so the printed sheets can be folded into a booklet.
+
+## API Endpoint
+
+`POST /api/v1/tools/booklet-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| perSheet | integer | No | `2` | Pages per sheet: `2`, `4`, `6`, or `8` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/booklet-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"perSheet": 2}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2400000
+}
+```
+
+## Notes
+
+- The default `perSheet: 2` places two pages side by side on each sheet, which is the standard booklet layout for duplex printing.
+- Blank pages are added automatically if the total page count is not a multiple of the sheet size.
+- Print the output double-sided on short-edge binding, then fold and staple.
diff --git a/apps/docs/tools/burn-subtitles.md b/apps/docs/tools/burn-subtitles.md
new file mode 100644
index 00000000..1e9ce3e2
--- /dev/null
+++ b/apps/docs/tools/burn-subtitles.md
@@ -0,0 +1,44 @@
+---
+description: Permanently render subtitles onto video frames.
+---
+
+# Burn Subtitles
+
+Permanently render (hard-code) subtitles from an SRT, VTT, or ASS file onto every frame of a video.
+
+## API Endpoint
+
+`POST /api/v1/tools/burn-subtitles`
+
+Accepts multipart form data with a video file and a subtitle file. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| fontSize | integer | No | `24` | Subtitle font size in pixels (8-72) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/burn-subtitles \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F "file=@subtitles.srt" \
+ -F 'settings={"fontSize": 28}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Upload two files: the first must be a video, the second must be a subtitle file (.srt, .vtt, or .ass).
+- Burned subtitles are permanently part of the video and cannot be turned off by the viewer. For toggleable subtitles, use the Embed Subtitles tool instead.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/change-fps.md b/apps/docs/tools/change-fps.md
new file mode 100644
index 00000000..07542216
--- /dev/null
+++ b/apps/docs/tools/change-fps.md
@@ -0,0 +1,45 @@
+---
+description: Change the frame rate of a video.
+---
+
+# Change FPS
+
+Change the frame rate of a video to a target value between 1 and 120 fps.
+
+## API Endpoint
+
+`POST /api/v1/tools/change-fps`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| fps | number | No | `30` | Target frame rate (1-120) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/change-fps \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"fps": 24}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 10200000
+}
+```
+
+## Notes
+
+- Lowering the frame rate drops frames and reduces file size. Increasing it duplicates frames to fill the gap but does not add real motion detail.
+- Common target values: 24 (cinema), 30 (web/broadcast), 60 (smooth playback).
+- The audio track is preserved at its original sample rate.
diff --git a/apps/docs/tools/chart-maker.md b/apps/docs/tools/chart-maker.md
new file mode 100644
index 00000000..158b276d
--- /dev/null
+++ b/apps/docs/tools/chart-maker.md
@@ -0,0 +1,49 @@
+---
+description: Create bar, line, or pie charts from CSV or JSON data.
+---
+
+# Chart Maker
+
+Create bar, line, or pie charts from CSV or JSON data. Returns a PNG image of the rendered chart.
+
+## API Endpoint
+
+`POST /api/v1/tools/chart-maker`
+
+Accepts multipart form data with a CSV or JSON file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| kind | string | No | `"bar"` | Chart type: `bar`, `line`, `pie` |
+| title | string | No | - | Chart title (max 120 characters) |
+| width | integer | No | `960` | Chart width in pixels (320--2048) |
+| height | integer | No | `540` | Chart height in pixels (240--1536) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/chart-maker \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@sales.csv" \
+ -F 'settings={"kind": "line", "title": "Monthly Sales", "width": 960, "height": 540}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/sales.png",
+ "originalSize": 1024,
+ "processedSize": 48500
+}
+```
+
+## Notes
+
+- Input must be a `.csv` or `.json` file. CSV files should have a header row with column names.
+- The first column is used as the category axis; remaining numeric columns become data series.
+- JSON input should be an array of objects with consistent keys.
+- Output is always a PNG image regardless of input format.
diff --git a/apps/docs/tools/circle-crop.md b/apps/docs/tools/circle-crop.md
new file mode 100644
index 00000000..39c71478
--- /dev/null
+++ b/apps/docs/tools/circle-crop.md
@@ -0,0 +1,52 @@
+---
+description: Crop an image to a centered circle with transparent corners.
+---
+
+# Circle Crop
+
+Crop an image to a centered circle with transparent corners. Supports adjustable zoom, offset, border, and output size.
+
+## API Endpoint
+
+`POST /api/v1/tools/circle-crop`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| zoom | number | No | `1` | Zoom factor (1-5); higher values crop tighter |
+| offsetX | number | No | `0.5` | Horizontal center position (0-1) |
+| offsetY | number | No | `0.5` | Vertical center position (0-1) |
+| borderWidth | integer | No | `0` | Border width in pixels (0-200) |
+| borderColor | string | No | `"#ffffff"` | Border hex color |
+| background | string | No | `"transparent"` | Corner fill: `"transparent"` or a hex color |
+| outputSize | integer | No | - | Final square dimension in pixels (16-4096) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/circle-crop \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"zoom": 1.2, "borderWidth": 4, "borderColor": "#333333"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.png",
+ "originalSize": 2450000,
+ "processedSize": 185000
+}
+```
+
+## Notes
+
+- Output is always PNG to preserve the transparent corners (unless `background` is set to a solid color).
+- The circle is inscribed within the shorter dimension of the image. Use `zoom` to crop tighter and `offsetX`/`offsetY` to shift the visible area.
+- When `outputSize` is provided, the result is resized to that square dimension after cropping.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/compress-pdf.md b/apps/docs/tools/compress-pdf.md
new file mode 100644
index 00000000..d8810619
--- /dev/null
+++ b/apps/docs/tools/compress-pdf.md
@@ -0,0 +1,59 @@
+---
+description: Shrink PDF file size by compressing embedded images.
+---
+
+# Compress PDF
+
+Reduce PDF file size by downsampling embedded images. Choose between a quality slider or a target file size.
+
+## API Endpoint
+
+`POST /api/v1/tools/compress-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| mode | string | No | `"quality"` | Compression mode: `quality` or `targetSize` |
+| quality | integer | No | - | Compression quality, 1-100 (higher = less compression). Used in `quality` mode |
+| targetSizeKb | number | No | - | Target file size in kilobytes. Used in `targetSize` mode |
+
+## Example Request
+
+Compress by quality:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/compress-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"mode": "quality", "quality": 60}'
+```
+
+Compress to a target size:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/compress-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"mode": "targetSize", "targetSizeKb": 500}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 5200000,
+ "processedSize": 1800000
+}
+```
+
+## Notes
+
+- In `quality` mode, lower values produce smaller files with more image degradation.
+- In `targetSize` mode, a binary search finds the highest DPI that fits the requested size.
+- If compression would enlarge the file, the original bytes are returned unchanged.
+- Text and vector content are not affected; only embedded raster images are downsampled.
diff --git a/apps/docs/tools/compress-video.md b/apps/docs/tools/compress-video.md
new file mode 100644
index 00000000..c6a5883d
--- /dev/null
+++ b/apps/docs/tools/compress-video.md
@@ -0,0 +1,44 @@
+---
+description: Shrink video file size with quality control.
+---
+
+# Compress Video
+
+Shrink video file size using configurable compression strength and optional resolution downscaling.
+
+## API Endpoint
+
+`POST /api/v1/tools/compress-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| quality | string | No | `"balanced"` | Compression strength: `light`, `balanced`, `strong` |
+| resolution | string | No | `"original"` | Output resolution: `original`, `1080p`, `720p`, `480p` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/compress-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"quality": "strong", "resolution": "720p"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- The `light` preset preserves near-original quality. The `strong` preset reduces file size aggressively at the cost of visual fidelity.
+- Downscaling resolution (e.g. from 4K to 720p) compounds with compression for significant size reduction.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/convert-audio.md b/apps/docs/tools/convert-audio.md
new file mode 100644
index 00000000..2d413ea0
--- /dev/null
+++ b/apps/docs/tools/convert-audio.md
@@ -0,0 +1,46 @@
+---
+description: Convert audio between MP3, WAV, OGG, FLAC, and M4A formats.
+---
+
+# Convert Audio
+
+Convert audio files between common formats including MP3, WAV, OGG, FLAC, and M4A, with configurable output bitrate.
+
+## API Endpoint
+
+`POST /api/v1/tools/convert-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | No | `"mp3"` | Output format: `mp3`, `wav`, `ogg`, `flac`, `m4a` |
+| bitrateKbps | integer | No | `192` | Output bitrate in kbps (32 to 320) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/convert-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"format": "flac", "bitrateKbps": 256}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.flac",
+ "originalSize": 4500000,
+ "processedSize": 8200000
+}
+```
+
+## Notes
+
+- Supported input formats include MP3, WAV, OGG, FLAC, AAC, M4A, WMA, AIFF, and OPUS.
+- Bitrate only applies to lossy formats (MP3, OGG, M4A). Lossless formats like WAV and FLAC ignore this setting.
+- The output filename keeps the original name with the new extension.
diff --git a/apps/docs/tools/convert-document.md b/apps/docs/tools/convert-document.md
new file mode 100644
index 00000000..e9cb4cd8
--- /dev/null
+++ b/apps/docs/tools/convert-document.md
@@ -0,0 +1,46 @@
+---
+description: Convert between Word, OpenDocument, RTF, and plain text formats.
+---
+
+# Convert Document
+
+Convert documents between Word (DOCX), OpenDocument (ODT), RTF, and plain text formats using LibreOffice.
+
+## API Endpoint
+
+`POST /api/v1/tools/convert-document`
+
+Accepts multipart form data with a Word/ODT/RTF/TXT file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | Yes | - | Output format: `docx`, `odt`, `rtf`, `txt` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/convert-document \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.docx" \
+ -F 'settings={"format": "odt"}'
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.docx`, `.doc`, `.odt`, `.rtf`, `.txt`.
+- Conversion is handled by LibreOffice running headless on the server.
+- Complex formatting (macros, embedded objects) may not survive conversion between formats.
+- The output format must differ from the input format.
diff --git a/apps/docs/tools/convert-presentation.md b/apps/docs/tools/convert-presentation.md
new file mode 100644
index 00000000..252e3bd2
--- /dev/null
+++ b/apps/docs/tools/convert-presentation.md
@@ -0,0 +1,46 @@
+---
+description: Convert between PowerPoint and OpenDocument presentation formats.
+---
+
+# Convert Presentation
+
+Convert presentations between PowerPoint (PPTX) and OpenDocument Presentation (ODP) formats.
+
+## API Endpoint
+
+`POST /api/v1/tools/convert-presentation`
+
+Accepts multipart form data with a PowerPoint/ODP file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | Yes | - | Output format: `pptx`, `odp` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/convert-presentation \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@slides.pptx" \
+ -F 'settings={"format": "odp"}'
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.pptx`, `.ppt`, `.odp`.
+- Conversion is handled by LibreOffice running headless on the server.
+- Animations and transition effects may not be preserved across formats.
+- The output format must differ from the input format.
diff --git a/apps/docs/tools/convert-spreadsheet.md b/apps/docs/tools/convert-spreadsheet.md
new file mode 100644
index 00000000..20510e7d
--- /dev/null
+++ b/apps/docs/tools/convert-spreadsheet.md
@@ -0,0 +1,46 @@
+---
+description: Convert between Excel, OpenDocument, and CSV formats.
+---
+
+# Convert Spreadsheet
+
+Convert spreadsheets between Excel (XLSX), OpenDocument Spreadsheet (ODS), and CSV formats. Multi-sheet workbooks export the first sheet when converting to CSV.
+
+## API Endpoint
+
+`POST /api/v1/tools/convert-spreadsheet`
+
+Accepts multipart form data with an Excel/ODS/CSV file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | Yes | - | Output format: `xlsx`, `ods`, `csv` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/convert-spreadsheet \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@data.xlsx" \
+ -F 'settings={"format": "csv"}'
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.xlsx`, `.xls`, `.ods`, `.csv`.
+- When converting a multi-sheet workbook to CSV, only the first sheet is exported.
+- Formulas are evaluated and exported as static values in CSV output.
+- The output format must differ from the input format.
diff --git a/apps/docs/tools/convert-video.md b/apps/docs/tools/convert-video.md
new file mode 100644
index 00000000..98368a1e
--- /dev/null
+++ b/apps/docs/tools/convert-video.md
@@ -0,0 +1,44 @@
+---
+description: Convert videos between MP4, MOV, and WebM.
+---
+
+# Convert Video
+
+Convert videos between MP4, MOV, and WebM formats with configurable quality presets.
+
+## API Endpoint
+
+`POST /api/v1/tools/convert-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | No | `"mp4"` | Output format: `mp4`, `mov`, `webm` |
+| quality | string | No | `"balanced"` | Quality preset: `high`, `balanced`, `small` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/convert-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"format": "webm", "quality": "balanced"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- The `high` quality preset produces the best visual fidelity but larger files. The `small` preset aggressively compresses for minimum file size.
+- WebM output uses VP9 encoding. MP4 and MOV use H.264.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/create-zip.md b/apps/docs/tools/create-zip.md
new file mode 100644
index 00000000..b3140c50
--- /dev/null
+++ b/apps/docs/tools/create-zip.md
@@ -0,0 +1,45 @@
+---
+description: Bundle multiple files into a single ZIP archive.
+---
+
+# Create ZIP
+
+Bundle multiple files of any type into a single ZIP archive. Duplicate filenames are automatically deduplicated.
+
+## API Endpoint
+
+`POST /api/v1/tools/create-zip`
+
+Accepts multipart form data with two or more files. No settings field is required.
+
+## Parameters
+
+This tool has no configurable parameters. Upload 2--50 files of any type to bundle.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/create-zip \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.pdf" \
+ -F "file=@data.csv" \
+ -F "file=@photo.jpg"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/archive.zip",
+ "originalSize": 3500000,
+ "processedSize": 2800000
+}
+```
+
+## Notes
+
+- Requires between 2 and 50 input files.
+- Any file type is accepted; there are no restrictions on input format.
+- If multiple files share the same name, they are automatically deduplicated with numeric suffixes.
+- The output archive uses standard ZIP compression (deflate).
diff --git a/apps/docs/tools/crop-pdf.md b/apps/docs/tools/crop-pdf.md
new file mode 100644
index 00000000..bb8d4ff2
--- /dev/null
+++ b/apps/docs/tools/crop-pdf.md
@@ -0,0 +1,45 @@
+---
+description: Crop all pages of a PDF with a uniform margin.
+---
+
+# Crop PDF
+
+Crop all pages of a PDF by applying a uniform margin, trimming content from each edge equally.
+
+## API Endpoint
+
+`POST /api/v1/tools/crop-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| margin | number | No | `20` | Uniform crop margin in points (0-2000) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/crop-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"margin": 50}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2440000
+}
+```
+
+## Notes
+
+- The margin value is in PDF points (1 point = 1/72 inch).
+- The same margin is applied to all four edges of every page.
+- A margin of `0` removes all existing crop margins, showing the full media box.
diff --git a/apps/docs/tools/crop-video.md b/apps/docs/tools/crop-video.md
new file mode 100644
index 00000000..f5f2d30b
--- /dev/null
+++ b/apps/docs/tools/crop-video.md
@@ -0,0 +1,48 @@
+---
+description: Crop a region out of a video.
+---
+
+# Crop Video
+
+Crop a rectangular region out of a video by specifying the region's size and position.
+
+## API Endpoint
+
+`POST /api/v1/tools/crop-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| width | integer | Yes | - | Crop region width in pixels (minimum 16) |
+| height | integer | Yes | - | Crop region height in pixels (minimum 16) |
+| x | integer | No | `0` | Horizontal offset from the top-left corner |
+| y | integer | No | `0` | Vertical offset from the top-left corner |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/crop-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"width": 640, "height": 480, "x": 100, "y": 50}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 5200000
+}
+```
+
+## Notes
+
+- The crop region must fit within the video dimensions. If `x + width` or `y + height` exceeds the source size, the request returns a 400 error.
+- Minimum crop size is 16x16 pixels.
+- Dimensions are rounded to even numbers as required by most video codecs.
diff --git a/apps/docs/tools/csv-excel.md b/apps/docs/tools/csv-excel.md
new file mode 100644
index 00000000..446d44ce
--- /dev/null
+++ b/apps/docs/tools/csv-excel.md
@@ -0,0 +1,56 @@
+---
+description: Convert between CSV and Excel (XLSX), both directions.
+---
+
+# CSV to Excel
+
+Convert between CSV and Excel (XLSX) formats in both directions. Upload a CSV or TSV file to get XLSX, or upload an XLSX file to get CSV.
+
+## API Endpoint
+
+`POST /api/v1/tools/csv-excel`
+
+Accepts multipart form data with a CSV, TSV, or XLSX file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| sheet | integer | No | `1` | Worksheet number to export when converting from XLSX (min 1) |
+
+## Example Request
+
+CSV to Excel:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/csv-excel \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@data.csv" \
+ -F 'settings={"sheet": 1}'
+```
+
+Excel to CSV:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/csv-excel \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.xlsx" \
+ -F 'settings={"sheet": 2}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/data.xlsx",
+ "originalSize": 2048,
+ "processedSize": 5120
+}
+```
+
+## Notes
+
+- Conversion direction is auto-detected from the input file extension: `.csv` or `.tsv` produces `.xlsx`, and `.xlsx` produces `.csv`.
+- The `sheet` parameter only applies when converting from XLSX. It selects which worksheet to export.
+- TSV (tab-separated values) files are supported alongside CSV.
diff --git a/apps/docs/tools/csv-json.md b/apps/docs/tools/csv-json.md
new file mode 100644
index 00000000..4a0b2366
--- /dev/null
+++ b/apps/docs/tools/csv-json.md
@@ -0,0 +1,57 @@
+---
+description: Convert between CSV and JSON, both directions.
+---
+
+# CSV to JSON
+
+Convert between CSV and JSON formats in both directions. Upload a CSV or TSV file to get a JSON array of objects, or upload a JSON array to get a CSV file.
+
+## API Endpoint
+
+`POST /api/v1/tools/csv-json`
+
+Accepts multipart form data with a CSV, TSV, or JSON file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| pretty | boolean | No | `true` | Pretty-print JSON output with indentation |
+
+## Example Request
+
+CSV to JSON:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/csv-json \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@users.csv" \
+ -F 'settings={"pretty": true}'
+```
+
+JSON to CSV:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/csv-json \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@users.json" \
+ -F 'settings={}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/users.json",
+ "originalSize": 1500,
+ "processedSize": 2200
+}
+```
+
+## Notes
+
+- Conversion direction is auto-detected from the input file extension: `.csv` or `.tsv` produces `.json`, and `.json` produces `.csv`.
+- The `pretty` parameter only affects JSON output. When set to `false`, the output is a compact single-line JSON string.
+- JSON input must be an array of objects with consistent keys. Each object becomes a row, and each key becomes a column header.
+- TSV (tab-separated values) files are supported alongside CSV.
diff --git a/apps/docs/tools/duotone.md b/apps/docs/tools/duotone.md
new file mode 100644
index 00000000..e499f4b6
--- /dev/null
+++ b/apps/docs/tools/duotone.md
@@ -0,0 +1,47 @@
+---
+description: Apply a two-color duotone effect with custom shadow and highlight colors.
+---
+
+# Duotone
+
+Apply a two-color duotone effect to an image. The image is converted to grayscale, then mapped to a gradient between the shadow color (dark tones) and the highlight color (bright tones).
+
+## API Endpoint
+
+`POST /api/v1/tools/duotone`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| shadow | string | No | `"#1e3a8a"` | Shadow hex color (applied to dark tones) |
+| highlight | string | No | `"#fbbf24"` | Highlight hex color (applied to bright tones) |
+| intensity | integer | No | `100` | Effect intensity (0-100); 0 returns the original, 100 applies the full duotone |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/duotone \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"shadow": "#0f172a", "highlight": "#f97316", "intensity": 80}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.jpg",
+ "originalSize": 2450000,
+ "processedSize": 1870000
+}
+```
+
+## Notes
+
+- Output format matches the input format. HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
+- An `intensity` of less than 100 blends the duotone result with the original image, allowing for subtler effects.
+- Popular duotone combinations include navy/gold, teal/coral, and purple/pink.
diff --git a/apps/docs/tools/embed-subtitles.md b/apps/docs/tools/embed-subtitles.md
new file mode 100644
index 00000000..46a66608
--- /dev/null
+++ b/apps/docs/tools/embed-subtitles.md
@@ -0,0 +1,46 @@
+---
+description: Mux a subtitle track into the video container.
+---
+
+# Embed Subtitles
+
+Mux a subtitle file into the video container as a soft subtitle track that viewers can toggle on or off.
+
+## API Endpoint
+
+`POST /api/v1/tools/embed-subtitles`
+
+Accepts multipart form data with a video file and a subtitle file, plus a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| language | string | No | `"eng"` | ISO 639-2/B language code (3 lowercase letters, e.g. `"eng"`, `"fra"`, `"deu"`) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/embed-subtitles \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F "file=@subtitles.srt" \
+ -F 'settings={"language": "fra"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12520000
+}
+```
+
+## Notes
+
+- Upload two files: the first must be a video, the second must be a subtitle file (.srt, .vtt, or .ass).
+- Embedded (soft) subtitles can be toggled by the viewer in their media player. For permanently visible subtitles, use the Burn Subtitles tool instead.
+- The language code is stored as metadata in the container and helps media players label the subtitle track.
diff --git a/apps/docs/tools/epub-convert.md b/apps/docs/tools/epub-convert.md
new file mode 100644
index 00000000..c511d818
--- /dev/null
+++ b/apps/docs/tools/epub-convert.md
@@ -0,0 +1,46 @@
+---
+description: Convert an EPUB to PDF, DOCX, HTML, or Markdown.
+---
+
+# Convert EPUB
+
+Convert an EPUB e-book to PDF, Word (DOCX), HTML, or Markdown. Remote resources inside the book are not fetched.
+
+## API Endpoint
+
+`POST /api/v1/tools/epub-convert`
+
+Accepts multipart form data with an EPUB file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | Yes | - | Output format: `pdf`, `docx`, `html`, `md` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/epub-convert \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@book.epub" \
+ -F 'settings={"format": "pdf"}'
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input format: `.epub`.
+- Remote resources embedded in the EPUB (external images, fonts) are not fetched for security.
+- Image fidelity in the converted output may vary depending on the EPUB structure.
+- Conversion is handled by Pandoc on the server.
diff --git a/apps/docs/tools/excel-to-pdf.md b/apps/docs/tools/excel-to-pdf.md
new file mode 100644
index 00000000..6e13e9e7
--- /dev/null
+++ b/apps/docs/tools/excel-to-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Convert spreadsheets to PDF.
+---
+
+# Excel to PDF
+
+Convert Excel, OpenDocument, or CSV spreadsheets to PDF. Wide sheets may paginate across multiple pages.
+
+## API Endpoint
+
+`POST /api/v1/tools/excel-to-pdf`
+
+Accepts multipart form data with an Excel/ODS/CSV file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a spreadsheet and it will be converted to PDF.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/excel-to-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@budget.xlsx"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.xlsx`, `.xls`, `.ods`, `.csv`.
+- Wide sheets may be split across multiple pages in the resulting PDF.
+- Charts and conditional formatting are rendered in the PDF output.
+- Conversion is handled by LibreOffice running headless on the server.
diff --git a/apps/docs/tools/extract-audio.md b/apps/docs/tools/extract-audio.md
new file mode 100644
index 00000000..ec2c6a0a
--- /dev/null
+++ b/apps/docs/tools/extract-audio.md
@@ -0,0 +1,45 @@
+---
+description: Pull the audio track out of a video.
+---
+
+# Extract Audio
+
+Extract the audio track from a video file and save it as MP3, WAV, or M4A.
+
+## API Endpoint
+
+`POST /api/v1/tools/extract-audio`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | No | `"mp3"` | Output audio format: `mp3`, `wav`, `m4a` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/extract-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"format": "mp3"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp3",
+ "originalSize": 12500000,
+ "processedSize": 3200000
+}
+```
+
+## Notes
+
+- If the video has no audio track, the request returns a 400 error.
+- MP3 is lossy but widely compatible. WAV is lossless but large. M4A (AAC) offers a good balance of quality and size.
+- When the source audio is already AAC and the output format is M4A, the audio stream is copied without re-encoding.
diff --git a/apps/docs/tools/extract-pages.md b/apps/docs/tools/extract-pages.md
new file mode 100644
index 00000000..2f5a3ea0
--- /dev/null
+++ b/apps/docs/tools/extract-pages.md
@@ -0,0 +1,44 @@
+---
+description: Pull selected pages from a PDF into a new document.
+---
+
+# Extract Pages
+
+Pull selected pages from a PDF into a new, smaller document.
+
+## API Endpoint
+
+`POST /api/v1/tools/extract-pages`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| range | string | Yes | - | Page range in qpdf syntax, e.g. `"1-5,8,10-z"` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/extract-pages \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"range": "1-5,8,10-z"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 3200000,
+ "processedSize": 1100000
+}
+```
+
+## Notes
+
+- Page ranges use qpdf syntax: `1-5` for pages 1 through 5, `z` for the last page, and commas to combine ranges (e.g. `1-3,7,10-z`).
+- The extracted pages retain their original formatting, annotations, and links.
diff --git a/apps/docs/tools/extract-subtitles.md b/apps/docs/tools/extract-subtitles.md
new file mode 100644
index 00000000..5ee8df1c
--- /dev/null
+++ b/apps/docs/tools/extract-subtitles.md
@@ -0,0 +1,42 @@
+---
+description: Pull the subtitle track out of a video as an SRT file.
+---
+
+# Extract Subtitles
+
+Extract the embedded subtitle track from a video container and download it as an SRT file.
+
+## API Endpoint
+
+`POST /api/v1/tools/extract-subtitles`
+
+Accepts multipart form data with a video file. This tool has no configurable settings.
+
+## Parameters
+
+This tool has no parameters. It extracts the first subtitle track found in the video container.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/extract-subtitles \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.srt",
+ "originalSize": 12500000,
+ "processedSize": 4500
+}
+```
+
+## Notes
+
+- The video must contain an embedded subtitle track. If no subtitle track is found, the request returns a 400 error.
+- If the video has multiple subtitle tracks, the first one is extracted.
+- The output format is SRT regardless of the original subtitle format in the container.
diff --git a/apps/docs/tools/extract-zip.md b/apps/docs/tools/extract-zip.md
new file mode 100644
index 00000000..dce284a0
--- /dev/null
+++ b/apps/docs/tools/extract-zip.md
@@ -0,0 +1,43 @@
+---
+description: Safely extract files from a ZIP archive with bomb protection.
+---
+
+# Extract ZIP
+
+Safely extract files from a ZIP archive. Single-file archives return the contained file directly; multi-file archives return a flat ZIP with the extracted contents.
+
+## API Endpoint
+
+`POST /api/v1/tools/extract-zip`
+
+Accepts multipart form data with a ZIP file. No settings field is required.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a `.zip` file to extract.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/extract-zip \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@archive.zip"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/archive.zip",
+ "originalSize": 2800000,
+ "processedSize": 3500000
+}
+```
+
+## Notes
+
+- Only `.zip` files are accepted as input.
+- If the archive contains a single file, that file is returned directly (not wrapped in a ZIP).
+- If the archive contains multiple files, a flat ZIP is returned with all files extracted to the root level (nested directory structure is flattened).
+- Built-in bomb protection rejects archives with excessive compression ratios or file counts to prevent resource exhaustion.
diff --git a/apps/docs/tools/fade-audio.md b/apps/docs/tools/fade-audio.md
new file mode 100644
index 00000000..d6362a9a
--- /dev/null
+++ b/apps/docs/tools/fade-audio.md
@@ -0,0 +1,46 @@
+---
+description: Add fade-in and fade-out effects to audio.
+---
+
+# Fade Audio
+
+Add fade-in and fade-out effects to the beginning and end of an audio file.
+
+## API Endpoint
+
+`POST /api/v1/tools/fade-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| fadeInS | number | No | `1` | Fade-in duration in seconds (0 to 30) |
+| fadeOutS | number | No | `1` | Fade-out duration in seconds (0 to 30) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/fade-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"fadeInS": 2, "fadeOutS": 3}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- Set either value to `0` to skip that fade direction.
+- The fade duration should not exceed the audio length.
+- Output format matches the input format.
diff --git a/apps/docs/tools/flatten-pdf.md b/apps/docs/tools/flatten-pdf.md
new file mode 100644
index 00000000..99fe7754
--- /dev/null
+++ b/apps/docs/tools/flatten-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Bake forms and annotations into page content.
+---
+
+# Flatten PDF
+
+Bake interactive form fields and annotations into the page content, producing a static PDF that looks the same everywhere.
+
+## API Endpoint
+
+`POST /api/v1/tools/flatten-pdf`
+
+Accepts multipart form data with a PDF file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a PDF and all forms and annotations will be flattened.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/flatten-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@form.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/form.pdf",
+ "originalSize": 185000,
+ "processedSize": 172000
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- This is a fast (synchronous) tool that returns the result directly.
+- Form field values are preserved as static text in the output.
+- Annotations (comments, highlights, sticky notes) become part of the page content and can no longer be edited.
diff --git a/apps/docs/tools/gif-to-video.md b/apps/docs/tools/gif-to-video.md
new file mode 100644
index 00000000..38e16a5f
--- /dev/null
+++ b/apps/docs/tools/gif-to-video.md
@@ -0,0 +1,45 @@
+---
+description: Convert an animated GIF into an MP4 or WebM video.
+---
+
+# GIF to Video
+
+Convert an animated GIF into a compact MP4 or WebM video file.
+
+## API Endpoint
+
+`POST /api/v1/tools/gif-to-video`
+
+Accepts multipart form data with a GIF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | No | `"mp4"` | Output format: `mp4`, `webm` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/gif-to-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@animation.gif" \
+ -F 'settings={"format": "mp4"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/animation.mp4",
+ "originalSize": 8500000,
+ "processedSize": 950000
+}
+```
+
+## Notes
+
+- Converting GIF to video typically reduces file size by 80-90% while maintaining the same visual quality.
+- Only animated GIF files are accepted. Static images should use the image Convert tool.
+- MP4 uses H.264 encoding, WebM uses VP9.
diff --git a/apps/docs/tools/gif-webp.md b/apps/docs/tools/gif-webp.md
new file mode 100644
index 00000000..1cec40e6
--- /dev/null
+++ b/apps/docs/tools/gif-webp.md
@@ -0,0 +1,48 @@
+---
+description: Convert animated GIF to WebP and vice versa, preserving all frames.
+---
+
+# GIF/WebP Converter
+
+Convert animated GIF files to WebP and vice versa, preserving all frames and animation timing. WebP animations are typically 25-35% smaller than equivalent GIFs.
+
+## API Endpoint
+
+`POST /api/v1/tools/gif-webp`
+
+Accepts multipart form data with a GIF or WebP file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| quality | integer | No | `80` | Output quality for WebP encoding (1-100) |
+| lossless | boolean | No | `false` | Use lossless WebP compression |
+| resizePercent | integer | No | `100` | Scale the output by percentage (10-100) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/gif-webp \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@animation.gif" \
+ -F 'settings={"quality": 85, "resizePercent": 50}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/animation.webp",
+ "originalSize": 3500000,
+ "processedSize": 2200000
+}
+```
+
+## Notes
+
+- Only `.gif` and `.webp` files are accepted. Other image formats are not supported by this tool.
+- The conversion direction is automatic: GIF input produces WebP output, and WebP input produces GIF output.
+- The `quality` and `lossless` options only apply when encoding to WebP. When converting to GIF, the output uses the standard GIF palette.
+- Use `resizePercent` to reduce the dimensions (and file size) of large animations.
diff --git a/apps/docs/tools/grayscale-pdf.md b/apps/docs/tools/grayscale-pdf.md
new file mode 100644
index 00000000..2a1dce5e
--- /dev/null
+++ b/apps/docs/tools/grayscale-pdf.md
@@ -0,0 +1,41 @@
+---
+description: Convert all colors in a PDF to grayscale.
+---
+
+# Grayscale PDF
+
+Convert all colors in a PDF to grayscale, producing a black-and-white version of the document.
+
+## API Endpoint
+
+`POST /api/v1/tools/grayscale-pdf`
+
+Accepts multipart form data with a PDF file. No `settings` field is required.
+
+## Parameters
+
+This tool has no settings parameters. Upload the PDF file directly.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/grayscale-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 3200000,
+ "processedSize": 2800000
+}
+```
+
+## Notes
+
+- All color spaces (RGB, CMYK) are converted to grayscale, including embedded images, vector graphics, and text.
+- The output file is often smaller than the original because grayscale data requires fewer bytes per pixel.
diff --git a/apps/docs/tools/histogram.md b/apps/docs/tools/histogram.md
new file mode 100644
index 00000000..6d3423fc
--- /dev/null
+++ b/apps/docs/tools/histogram.md
@@ -0,0 +1,62 @@
+---
+description: Generate an RGB histogram chart with per-channel statistics from an image.
+---
+
+# Histogram
+
+Generate an RGB histogram chart from an image. Returns a PNG histogram image along with per-channel statistics and raw 256-bin histogram data in the response JSON.
+
+## API Endpoint
+
+`POST /api/v1/tools/histogram`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| scale | string | No | `"linear"` | Y-axis scale: `linear` or `log` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/histogram \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"scale": "linear"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/histogram.png",
+ "originalSize": 2450000,
+ "processedSize": 12000,
+ "bins": {
+ "r": [0, 12, 45, "... (256 values)"],
+ "g": [0, 8, 38, "... (256 values)"],
+ "b": [2, 15, 52, "... (256 values)"],
+ "lum": [0, 10, 40, "... (256 values)"]
+ },
+ "stats": {
+ "r": { "mean": 128, "median": 132, "stdev": 48.5 },
+ "g": { "mean": 119, "median": 121, "stdev": 44.2 },
+ "b": { "mean": 105, "median": 108, "stdev": 51.3 },
+ "lum": { "mean": 118, "median": 120, "stdev": 45.1 }
+ },
+ "mean": { "r": 128, "g": 119, "b": 105 },
+ "max": { "r": 4200, "g": 3800, "b": 4100 }
+}
+```
+
+## Notes
+
+- The `downloadUrl` points to a rendered PNG histogram chart showing the R, G, B, and luminance distributions.
+- `bins` contains raw 256-value arrays for each channel (red, green, blue, luminance), suitable for rendering custom visualizations.
+- `stats` provides mean, median, and standard deviation per channel.
+- `mean` and `max` are backward-compatible shorthand fields.
+- Use `log` scale when the histogram is dominated by a few peaks and you want to see detail in the lower bins.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before analysis.
diff --git a/apps/docs/tools/html-to-pdf.md b/apps/docs/tools/html-to-pdf.md
new file mode 100644
index 00000000..46884dfb
--- /dev/null
+++ b/apps/docs/tools/html-to-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Convert an HTML file to PDF.
+---
+
+# HTML to PDF
+
+Convert an HTML file to a styled PDF document. Remote resources (external images, stylesheets, scripts) are disabled for privacy.
+
+## API Endpoint
+
+`POST /api/v1/tools/html-to-pdf`
+
+Accepts multipart form data with an HTML file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload an HTML file and it will be converted to PDF.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/html-to-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@page.html"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.html`, `.htm`.
+- Remote resources (images, stylesheets, scripts referenced via URLs) are not fetched for privacy and security.
+- Inline styles and embedded images (data URIs) are preserved.
+- Conversion is handled by WeasyPrint on the server.
diff --git a/apps/docs/tools/image-pad.md b/apps/docs/tools/image-pad.md
new file mode 100644
index 00000000..d1ba8225
--- /dev/null
+++ b/apps/docs/tools/image-pad.md
@@ -0,0 +1,51 @@
+---
+description: Pad an image to a target aspect ratio with a solid color, transparent, or blurred background.
+---
+
+# Image Pad
+
+Pad an image to a target aspect ratio by adding a solid color, transparent, or blurred background around it. Useful for fitting images into fixed aspect ratios for social media or print without cropping.
+
+## API Endpoint
+
+`POST /api/v1/tools/image-pad`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| target | string | No | `"1:1"` | Target aspect ratio: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, or `custom` |
+| ratioW | integer | No | `1` | Custom ratio width (1-100, used when target is `custom`) |
+| ratioH | integer | No | `1` | Custom ratio height (1-100, used when target is `custom`) |
+| background | string | No | `"color"` | Background mode: `color`, `transparent`, or `blur` |
+| color | string | No | `"#ffffff"` | Background hex color (when background is `color`) |
+| padding | integer | No | `0` | Extra padding as percentage of canvas (0-50) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/image-pad \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"target": "16:9", "background": "blur", "padding": 5}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.jpg",
+ "originalSize": 2450000,
+ "processedSize": 3100000
+}
+```
+
+## Notes
+
+- The `blur` background mode creates a blurred copy of the original image as the pad fill, producing a visually cohesive result.
+- When using `transparent` background, the output is converted to PNG to preserve alpha.
+- Output format matches the input format unless transparency is involved. HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
+- Set `target` to `custom` and provide `ratioW` and `ratioH` for arbitrary aspect ratios (e.g., `ratioW: 3, ratioH: 2` for 3:2).
diff --git a/apps/docs/tools/images-to-video.md b/apps/docs/tools/images-to-video.md
new file mode 100644
index 00000000..d0394e3c
--- /dev/null
+++ b/apps/docs/tools/images-to-video.md
@@ -0,0 +1,51 @@
+---
+description: Turn a set of images into a slideshow video.
+---
+
+# Images to Video
+
+Turn a set of images into a slideshow video with configurable duration per image, resolution, and frame rate.
+
+## API Endpoint
+
+`POST /api/v1/tools/images-to-video`
+
+Accepts multipart form data with two or more image files and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| secondsPerImage | number | No | `2` | Display duration per image in seconds (0.5-10) |
+| resolution | string | No | `"720p"` | Output resolution: `1080p`, `720p`, `square` |
+| fps | integer | No | `30` | Output frame rate (10-60) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/images-to-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@slide1.jpg" \
+ -F "file=@slide2.jpg" \
+ -F "file=@slide3.jpg" \
+ -F "file=@slide4.jpg" \
+ -F 'settings={"secondsPerImage": 3, "resolution": "1080p"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/slideshow.mp4",
+ "originalSize": 3500000,
+ "processedSize": 1200000
+}
+```
+
+## Notes
+
+- Accepts 2-60 image files per request. Images appear in the video in upload order.
+- Images are resized and padded to fit the target resolution while preserving aspect ratio.
+- The `square` resolution option produces a 720x720 video, useful for social media.
+- Output format is always MP4 (H.264).
diff --git a/apps/docs/tools/json-xml.md b/apps/docs/tools/json-xml.md
new file mode 100644
index 00000000..91e9f038
--- /dev/null
+++ b/apps/docs/tools/json-xml.md
@@ -0,0 +1,56 @@
+---
+description: Convert between JSON and XML, both directions.
+---
+
+# JSON to XML
+
+Convert between JSON and XML formats in both directions. Upload a JSON file to get XML, or upload an XML file to get JSON.
+
+## API Endpoint
+
+`POST /api/v1/tools/json-xml`
+
+Accepts multipart form data with a JSON or XML file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| pretty | boolean | No | `true` | Pretty-print output with indentation |
+
+## Example Request
+
+JSON to XML:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/json-xml \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@config.json" \
+ -F 'settings={"pretty": true}'
+```
+
+XML to JSON:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/json-xml \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@config.xml" \
+ -F 'settings={"pretty": true}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/config.xml",
+ "originalSize": 850,
+ "processedSize": 1200
+}
+```
+
+## Notes
+
+- Conversion direction is auto-detected from the input file extension: `.json` produces `.xml`, and `.xml` produces `.json`.
+- The `pretty` parameter applies to both directions. When `false`, the output is compact with no indentation.
+- XML attributes and nested structures are preserved during round-trip conversion where possible.
diff --git a/apps/docs/tools/linearize-pdf.md b/apps/docs/tools/linearize-pdf.md
new file mode 100644
index 00000000..f32a5d64
--- /dev/null
+++ b/apps/docs/tools/linearize-pdf.md
@@ -0,0 +1,42 @@
+---
+description: Linearize a PDF for fast web viewing (progressive download).
+---
+
+# Web-Optimize PDF
+
+Linearize a PDF so it can be progressively downloaded and displayed in web browsers without waiting for the full file.
+
+## API Endpoint
+
+`POST /api/v1/tools/linearize-pdf`
+
+Accepts multipart form data with a PDF file. No `settings` field is required.
+
+## Parameters
+
+This tool has no settings parameters. Upload the PDF file directly.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/linearize-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2460000
+}
+```
+
+## Notes
+
+- Linearization rearranges the PDF's internal structure so the first page can render before the full file has downloaded.
+- The output file may be slightly larger than the input due to the added linearization data.
+- Already-linearized PDFs are re-linearized without issue.
diff --git a/apps/docs/tools/lqip-placeholder.md b/apps/docs/tools/lqip-placeholder.md
new file mode 100644
index 00000000..ec291fb5
--- /dev/null
+++ b/apps/docs/tools/lqip-placeholder.md
@@ -0,0 +1,59 @@
+---
+description: Generate a tiny low-quality image placeholder with base64 data URI.
+---
+
+# LQIP Placeholder
+
+Generate a tiny low-quality image placeholder (LQIP) from a source image. Returns a small placeholder file along with a base64 data URI, ready-to-use HTML `
` tag, and CSS `background-image` snippet for immediate embedding.
+
+## API Endpoint
+
+`POST /api/v1/tools/lqip-placeholder`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| width | integer | No | `16` | Target width in pixels (4-64) |
+| blur | number | No | `2` | Blur radius for the blur strategy (0-20) |
+| strategy | string | No | `"blur"` | Placeholder strategy: `blur`, `pixelate`, or `solid` |
+| format | string | No | `"webp"` | Output format: `webp`, `png`, or `jpeg` |
+| quality | integer | No | `50` | Output quality (1-100) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/lqip-placeholder \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"width": 20, "strategy": "blur", "format": "webp"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.webp",
+ "originalSize": 2450000,
+ "processedSize": 280,
+ "dataUri": "data:image/webp;base64,UklGR...",
+ "width": 20,
+ "height": 13,
+ "bytes": 280,
+ "strategy": "blur",
+ "html": "
",
+ "css": "background-image: url(data:image/webp;base64,UklGR...);"
+}
+```
+
+## Notes
+
+- The `dataUri` field contains the complete data URI, ready for use in `src` attributes or CSS without any additional requests.
+- The `html` and `css` fields provide copy-paste snippets for common use cases.
+- The `blur` strategy produces a soft, blurred thumbnail. The `pixelate` strategy creates a blocky mosaic. The `solid` strategy returns a single averaged color.
+- Typical placeholder sizes are 200-500 bytes, making them suitable for inlining directly in HTML.
+- Height is calculated automatically to preserve the source image's aspect ratio.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/markdown-to-docx.md b/apps/docs/tools/markdown-to-docx.md
new file mode 100644
index 00000000..a1d95b48
--- /dev/null
+++ b/apps/docs/tools/markdown-to-docx.md
@@ -0,0 +1,43 @@
+---
+description: Convert a Markdown file to a Word document (DOCX).
+---
+
+# Markdown to Word
+
+Convert a Markdown file to a Word document (DOCX), preserving headings, lists, code blocks, and other formatting.
+
+## API Endpoint
+
+`POST /api/v1/tools/markdown-to-docx`
+
+Accepts multipart form data with a Markdown file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a Markdown file and it will be converted to DOCX.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/markdown-to-docx \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@README.md"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/README.docx",
+ "originalSize": 4500,
+ "processedSize": 18200
+}
+```
+
+## Notes
+
+- Accepted input formats: `.md`, `.markdown`.
+- This is a fast (synchronous) tool that returns the result directly.
+- Headings, bold, italic, links, code blocks, and lists are mapped to Word styles.
+- Conversion is handled by Pandoc on the server.
diff --git a/apps/docs/tools/markdown-to-html.md b/apps/docs/tools/markdown-to-html.md
new file mode 100644
index 00000000..25a5d3ee
--- /dev/null
+++ b/apps/docs/tools/markdown-to-html.md
@@ -0,0 +1,43 @@
+---
+description: Convert a Markdown file to a standalone HTML page.
+---
+
+# Markdown to HTML
+
+Convert a Markdown file to a standalone HTML page. Remote images referenced in the source are left as-is in the output.
+
+## API Endpoint
+
+`POST /api/v1/tools/markdown-to-html`
+
+Accepts multipart form data with a Markdown file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a Markdown file and it will be converted to HTML.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/markdown-to-html \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@notes.md"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/notes.html",
+ "originalSize": 3200,
+ "processedSize": 5800
+}
+```
+
+## Notes
+
+- Accepted input formats: `.md`, `.markdown`.
+- This is a fast (synchronous) tool that returns the result directly.
+- The output is a self-contained HTML page with inline styles.
+- Remote image URLs in the Markdown source are preserved as-is and not fetched.
diff --git a/apps/docs/tools/markdown-to-pdf.md b/apps/docs/tools/markdown-to-pdf.md
new file mode 100644
index 00000000..3e6d78f8
--- /dev/null
+++ b/apps/docs/tools/markdown-to-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Convert a Markdown file to a styled PDF.
+---
+
+# Markdown to PDF
+
+Convert a Markdown file to a styled PDF document. Remote resources are disabled for privacy.
+
+## API Endpoint
+
+`POST /api/v1/tools/markdown-to-pdf`
+
+Accepts multipart form data with a Markdown file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a Markdown file and it will be converted to PDF.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/markdown-to-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.md"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.md`, `.markdown`.
+- Remote resources (images, stylesheets referenced via URLs) are not fetched for privacy and security.
+- The Markdown is first rendered to HTML, then converted to PDF via WeasyPrint.
+- Code blocks, tables, and other Markdown elements are styled in the PDF output.
diff --git a/apps/docs/tools/merge-audio.md b/apps/docs/tools/merge-audio.md
new file mode 100644
index 00000000..b9af04db
--- /dev/null
+++ b/apps/docs/tools/merge-audio.md
@@ -0,0 +1,48 @@
+---
+description: Combine multiple audio files into one sequential track.
+---
+
+# Merge Audio
+
+Combine two or more audio files into a single sequential track, concatenated in the order they are uploaded.
+
+## API Endpoint
+
+`POST /api/v1/tools/merge-audio`
+
+Accepts multipart form data with multiple audio files and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| format | string | No | `"mp3"` | Output format: `mp3`, `wav`, `flac`, `m4a` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/merge-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@intro.mp3" \
+ -F "file=@main.mp3" \
+ -F "file=@outro.mp3" \
+ -F 'settings={"format": "mp3"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/merged.mp3",
+ "originalSize": 9500000,
+ "processedSize": 9200000
+}
+```
+
+## Notes
+
+- Accepts 2 to 10 audio files per request.
+- Files are concatenated in upload order.
+- All input files are re-encoded to the chosen output format and sample rate for seamless joining.
+- Mixed input formats are supported (e.g. one WAV and one MP3).
diff --git a/apps/docs/tools/merge-csvs.md b/apps/docs/tools/merge-csvs.md
new file mode 100644
index 00000000..a804a758
--- /dev/null
+++ b/apps/docs/tools/merge-csvs.md
@@ -0,0 +1,45 @@
+---
+description: Combine multiple CSV or TSV files with matching columns into one.
+---
+
+# Merge CSVs
+
+Combine multiple CSV or TSV files with matching columns into a single merged file. All input files must have the same column headers.
+
+## API Endpoint
+
+`POST /api/v1/tools/merge-csvs`
+
+Accepts multipart form data with two or more CSV files. No settings field is required.
+
+## Parameters
+
+This tool has no configurable parameters. Upload 2--20 CSV or TSV files with matching column headers.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/merge-csvs \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@january.csv" \
+ -F "file=@february.csv" \
+ -F "file=@march.csv"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/merged.csv",
+ "originalSize": 30000,
+ "processedSize": 28500
+}
+```
+
+## Notes
+
+- Requires between 2 and 20 input files.
+- All files must share the same column headers. The merge will fail if columns do not match.
+- The header row is included once in the output; data rows from all files are concatenated in upload order.
+- Both CSV and TSV files are accepted, but all files in a single request should use the same delimiter.
diff --git a/apps/docs/tools/merge-pdf.md b/apps/docs/tools/merge-pdf.md
new file mode 100644
index 00000000..7db593c1
--- /dev/null
+++ b/apps/docs/tools/merge-pdf.md
@@ -0,0 +1,50 @@
+---
+description: Combine multiple PDFs into a single document.
+---
+
+# Merge PDFs
+
+Combine two or more PDF files into a single document, preserving the page order of each input file.
+
+## API Endpoint
+
+`POST /api/v1/tools/merge-pdf`
+
+Accepts multipart form data with two or more PDF files. No `settings` field is required.
+
+## Parameters
+
+This tool has no settings parameters. Simply upload two or more PDF files.
+
+| Constraint | Value |
+|------------|-------|
+| Minimum files | 2 |
+| Maximum files | 20 |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/merge-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document1.pdf" \
+ -F "file=@document2.pdf" \
+ -F "file=@document3.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/merged.pdf",
+ "originalSize": 4500000,
+ "processedSize": 4200000
+}
+```
+
+## Notes
+
+- Files are merged in the order they are uploaded.
+- At least two PDF files are required; the request will fail with a 400 error if fewer are provided.
+- The maximum number of input files is 20.
+- Encrypted PDFs must be unlocked before merging.
diff --git a/apps/docs/tools/merge-videos.md b/apps/docs/tools/merge-videos.md
new file mode 100644
index 00000000..7d6cdaeb
--- /dev/null
+++ b/apps/docs/tools/merge-videos.md
@@ -0,0 +1,43 @@
+---
+description: Join multiple video clips into one file.
+---
+
+# Merge Videos
+
+Join multiple video clips into a single MP4 file. All inputs are normalized to the first video's resolution and 30 fps.
+
+## API Endpoint
+
+`POST /api/v1/tools/merge-videos`
+
+Accepts multipart form data with two or more video files. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+This tool has no settings parameters. Upload 2-10 video files as multiple `file` parts.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/merge-videos \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@intro.mp4" \
+ -F "file=@main.mp4" \
+ -F "file=@outro.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Clips are concatenated in the order they are uploaded.
+- All clips are re-encoded to match the first clip's resolution, frame rate (30 fps), and codec (H.264). Mismatched inputs are automatically normalized.
+- Accepts 2-10 video files per request.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/mute-video.md b/apps/docs/tools/mute-video.md
new file mode 100644
index 00000000..d84c59b7
--- /dev/null
+++ b/apps/docs/tools/mute-video.md
@@ -0,0 +1,41 @@
+---
+description: Remove the audio track from a video.
+---
+
+# Mute Video
+
+Remove the audio track from a video, leaving only the visual stream.
+
+## API Endpoint
+
+`POST /api/v1/tools/mute-video`
+
+Accepts multipart form data with a video file. This tool has no configurable settings.
+
+## Parameters
+
+This tool has no parameters. It strips the audio track from the uploaded video.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/mute-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 8900000
+}
+```
+
+## Notes
+
+- The video stream is copied without re-encoding, so there is no quality loss.
+- If the input video has no audio track, the file is returned unchanged.
diff --git a/apps/docs/tools/noise-reduction.md b/apps/docs/tools/noise-reduction.md
new file mode 100644
index 00000000..b766353b
--- /dev/null
+++ b/apps/docs/tools/noise-reduction.md
@@ -0,0 +1,45 @@
+---
+description: Reduce background noise from audio with FFT-based denoising.
+---
+
+# Noise Reduction
+
+Reduce background noise in an audio file using FFT-based denoising with selectable strength.
+
+## API Endpoint
+
+`POST /api/v1/tools/noise-reduction`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| strength | string | No | `"medium"` | Denoising strength: `light`, `medium`, `strong` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/noise-reduction \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"strength": "strong"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- `light` preserves more detail but removes less noise. `strong` removes more noise but may introduce subtle artifacts.
+- Best results on recordings with consistent background noise (fan hum, air conditioning, static).
+- Output format matches the input format.
diff --git a/apps/docs/tools/normalize-audio.md b/apps/docs/tools/normalize-audio.md
new file mode 100644
index 00000000..fb57610b
--- /dev/null
+++ b/apps/docs/tools/normalize-audio.md
@@ -0,0 +1,43 @@
+---
+description: Even out loudness to broadcast standard levels (EBU R128).
+---
+
+# Normalize Audio
+
+Even out audio loudness to broadcast standard levels using EBU R128 normalization (-16 LUFS).
+
+## API Endpoint
+
+`POST /api/v1/tools/normalize-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+This tool has no configurable parameters. It applies EBU R128 loudness normalization automatically.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/normalize-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- Uses the EBU R128 loudness standard, targeting -16 LUFS.
+- Ideal for podcasts, audiobooks, and broadcast content where consistent loudness is important.
+- The source sample rate is preserved in the output.
+- Output format matches the input format.
diff --git a/apps/docs/tools/nup-pdf.md b/apps/docs/tools/nup-pdf.md
new file mode 100644
index 00000000..1bc0e757
--- /dev/null
+++ b/apps/docs/tools/nup-pdf.md
@@ -0,0 +1,45 @@
+---
+description: Arrange multiple PDF pages per sheet (2-up, 4-up, etc.).
+---
+
+# N-up PDF
+
+Arrange multiple pages per sheet to save paper when printing, such as 2-up or 4-up layouts.
+
+## API Endpoint
+
+`POST /api/v1/tools/nup-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| perSheet | integer | No | `2` | Pages per sheet: `2`, `3`, `4`, `8`, `9`, `12`, or `16` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/nup-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"perSheet": 4}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2300000
+}
+```
+
+## Notes
+
+- Pages are arranged in reading order (left to right, top to bottom).
+- The output page size matches the original; individual pages are scaled down to fit the grid.
+- A 20-page document with `perSheet: 4` produces a 5-page output.
diff --git a/apps/docs/tools/ocr-pdf.md b/apps/docs/tools/ocr-pdf.md
new file mode 100644
index 00000000..89dd7ceb
--- /dev/null
+++ b/apps/docs/tools/ocr-pdf.md
@@ -0,0 +1,50 @@
+---
+description: Extract text from PDF documents using AI-powered OCR.
+---
+
+# PDF OCR
+
+Extract text from PDF documents using AI-powered optical character recognition. Supports multiple quality tiers and languages. Requires the OCR feature bundle to be installed.
+
+## API Endpoint
+
+`POST /api/v1/tools/ocr-pdf`
+
+Accepts multipart form data with a PDF file and an optional JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| quality | string | No | `"balanced"` | OCR quality tier: `fast`, `balanced`, `best` |
+| language | string | No | `"auto"` | Document language: `auto`, `en`, `de`, `fr`, `es`, `zh`, `ja`, `ko` |
+| pages | string | No | `"all"` | Page selection, e.g. `"all"`, `"1-3"`, `"1,3,5"` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/ocr-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@scanned.pdf" \
+ -F 'settings={"quality": "best", "language": "en", "pages": "1-5"}'
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- This is an AI tool that requires the **OCR feature bundle** to be installed. If the bundle is not installed, the API returns `501 Not Implemented`.
+- The `fast` quality tier uses a lighter model for quicker processing; `best` uses a more accurate model at the cost of speed.
+- The `auto` language setting attempts to detect the document language automatically.
+- You can target specific pages using ranges (`"1-3"`), comma-separated lists (`"1,3,5"`), or `"all"` for every page.
+- For PDFs that already contain selectable text, consider using the faster [PDF to Text](./pdf-to-text) tool instead.
diff --git a/apps/docs/tools/organize-pdf.md b/apps/docs/tools/organize-pdf.md
new file mode 100644
index 00000000..e635811b
--- /dev/null
+++ b/apps/docs/tools/organize-pdf.md
@@ -0,0 +1,45 @@
+---
+description: Reorder pages in a PDF with an explicit page order.
+---
+
+# Organize PDF
+
+Reorder pages in a PDF by specifying the desired page sequence.
+
+## API Endpoint
+
+`POST /api/v1/tools/organize-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| order | string | Yes | - | Desired page order in qpdf syntax, e.g. `"3,1,2,5-z"` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/organize-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"order": "3,1,2,5-z"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2450000
+}
+```
+
+## Notes
+
+- Page ranges use qpdf syntax: `3,1,2` reorders the first three pages, and `5-z` appends pages 5 through the last page.
+- Pages can be duplicated by listing them more than once (e.g. `"1,1,2,3"` duplicates page 1).
+- Pages not listed in the order string are omitted from the output.
diff --git a/apps/docs/tools/pdf-metadata.md b/apps/docs/tools/pdf-metadata.md
new file mode 100644
index 00000000..037f10d6
--- /dev/null
+++ b/apps/docs/tools/pdf-metadata.md
@@ -0,0 +1,58 @@
+---
+description: Read and write PDF document metadata.
+---
+
+# PDF Metadata
+
+Read and update PDF document metadata fields such as title, author, subject, and keywords. When no settings are provided, the existing metadata is returned without modification.
+
+## API Endpoint
+
+`POST /api/v1/tools/pdf-metadata`
+
+Accepts multipart form data with a PDF file and an optional JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| title | string | No | - | Document title (max 500 characters) |
+| author | string | No | - | Document author (max 500 characters) |
+| subject | string | No | - | Document subject (max 500 characters) |
+| keywords | string | No | - | Document keywords (max 500 characters) |
+
+All parameters are optional. Omitted fields are left unchanged.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pdf-metadata \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.pdf" \
+ -F 'settings={"title": "Q2 Report", "author": "Finance Team"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/report.pdf",
+ "originalSize": 245000,
+ "processedSize": 245200,
+ "metadata": {
+ "title": "Q2 Report",
+ "author": "Finance Team",
+ "subject": "",
+ "keywords": ""
+ }
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- This is a fast (synchronous) tool that returns the result directly.
+- The `metadata` field in the response contains the resulting metadata after any updates.
+- To read metadata without modifying it, omit the `settings` field or send an empty object.
+- Each metadata field is limited to 500 characters.
diff --git a/apps/docs/tools/pdf-page-numbers.md b/apps/docs/tools/pdf-page-numbers.md
new file mode 100644
index 00000000..0a710805
--- /dev/null
+++ b/apps/docs/tools/pdf-page-numbers.md
@@ -0,0 +1,51 @@
+---
+description: Add page numbers to every page of a PDF.
+---
+
+# PDF Page Numbers
+
+Add "Page N of M" page numbers to every page of a PDF.
+
+## API Endpoint
+
+`POST /api/v1/tools/pdf-page-numbers`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| position | string | No | `"bc"` | Page number placement: `bl`, `bc`, `br`, `tl`, `tc`, `tr` |
+| fontSize | integer | No | `10` | Font size in points (6-24) |
+
+### Position Values
+
+- `tl` top-left, `tc` top-center, `tr` top-right
+- `bl` bottom-left, `bc` bottom-center, `br` bottom-right
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pdf-page-numbers \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"position": "bc", "fontSize": 12}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2470000
+}
+```
+
+## Notes
+
+- Page numbers are rendered in the format "Page 1 of 10".
+- Numbers are added to every page, including any existing title or cover pages.
+- The default position `"bc"` places numbers at the bottom center of each page.
diff --git a/apps/docs/tools/pdf-to-text.md b/apps/docs/tools/pdf-to-text.md
new file mode 100644
index 00000000..39b1d25d
--- /dev/null
+++ b/apps/docs/tools/pdf-to-text.md
@@ -0,0 +1,44 @@
+---
+description: Extract plain text from a PDF.
+---
+
+# PDF to Text
+
+Extract all readable plain text from a PDF document into a text file.
+
+## API Endpoint
+
+`POST /api/v1/tools/pdf-to-text`
+
+Accepts multipart form data with a PDF file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a PDF and its text content will be extracted.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pdf-to-text \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/report.txt",
+ "originalSize": 520000,
+ "processedSize": 14300,
+ "chars": 14300
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- This is a fast (synchronous) tool that returns the result directly.
+- The `chars` field in the response indicates the number of characters extracted.
+- Only digitally embedded text is extracted. For scanned documents or image-based PDFs, use the [PDF OCR](./ocr-pdf) tool instead.
diff --git a/apps/docs/tools/pdf-to-word.md b/apps/docs/tools/pdf-to-word.md
new file mode 100644
index 00000000..574f7312
--- /dev/null
+++ b/apps/docs/tools/pdf-to-word.md
@@ -0,0 +1,43 @@
+---
+description: Convert a PDF to a Word document (DOCX).
+---
+
+# PDF to Word
+
+Convert a text-based PDF to a Word document (DOCX). Best suited for PDFs with selectable text; scanned pages will need OCR first.
+
+## API Endpoint
+
+`POST /api/v1/tools/pdf-to-word`
+
+Accepts multipart form data with a PDF file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a PDF and it will be converted to DOCX.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pdf-to-word \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.pdf"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- Works best with text-based PDFs. Scanned or image-only pages will produce empty or minimal output; use [PDF OCR](./ocr-pdf) to add a text layer first.
+- Conversion is handled by LibreOffice running headless on the server.
+- Complex layouts (multi-column, overlapping elements) may not convert perfectly.
diff --git a/apps/docs/tools/pdfa-convert.md b/apps/docs/tools/pdfa-convert.md
new file mode 100644
index 00000000..8c4201a8
--- /dev/null
+++ b/apps/docs/tools/pdfa-convert.md
@@ -0,0 +1,42 @@
+---
+description: Convert a PDF to archival PDF/A-2 format for long-term preservation.
+---
+
+# PDF/A Convert
+
+Convert a PDF to the PDF/A-2 archival format, suitable for long-term preservation and regulatory compliance.
+
+## API Endpoint
+
+`POST /api/v1/tools/pdfa-convert`
+
+Accepts multipart form data with a PDF file. No `settings` field is required.
+
+## Parameters
+
+This tool has no settings parameters. Upload the PDF file directly.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pdfa-convert \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2600000
+}
+```
+
+## Notes
+
+- The output conforms to the PDF/A-2 standard.
+- PDF/A embeds all fonts and disallows external references, so the output file may be larger than the original.
+- Encryption and JavaScript are stripped during conversion, as they are not permitted by the PDF/A standard.
diff --git a/apps/docs/tools/pitch-shift.md b/apps/docs/tools/pitch-shift.md
new file mode 100644
index 00000000..47e99098
--- /dev/null
+++ b/apps/docs/tools/pitch-shift.md
@@ -0,0 +1,46 @@
+---
+description: Raise or lower audio pitch by semitones without changing speed.
+---
+
+# Pitch Shift
+
+Raise or lower the pitch of an audio file by a number of semitones without changing its playback speed.
+
+## API Endpoint
+
+`POST /api/v1/tools/pitch-shift`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| semitones | integer | No | `3` | Semitones to shift (-12 to 12). Must be nonzero. |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pitch-shift \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"semitones": -5}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- Positive values raise the pitch; negative values lower it.
+- A shift of 12 semitones equals one octave up; -12 equals one octave down.
+- Playback duration stays the same regardless of the shift amount.
+- Output format matches the input format.
diff --git a/apps/docs/tools/pixelate.md b/apps/docs/tools/pixelate.md
new file mode 100644
index 00000000..6514b192
--- /dev/null
+++ b/apps/docs/tools/pixelate.md
@@ -0,0 +1,66 @@
+---
+description: Apply a pixelation effect to the full image or a specific region.
+---
+
+# Pixelate
+
+Apply a pixelation effect to an entire image or a specific rectangular region. Useful for obscuring sensitive content like faces, license plates, or personal information.
+
+## API Endpoint
+
+`POST /api/v1/tools/pixelate`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| blockSize | integer | No | `12` | Pixel block size (2-128); larger values produce coarser pixelation |
+| region | object | No | - | Restrict pixelation to a rectangle (see below) |
+
+### Region Object
+
+| Field | Type | Required | Description |
+|-------|------|----------|-------------|
+| left | integer | Yes | Left offset in pixels (>= 0) |
+| top | integer | Yes | Top offset in pixels (>= 0) |
+| width | integer | Yes | Region width in pixels (>= 1) |
+| height | integer | Yes | Region height in pixels (>= 1) |
+
+## Example Request
+
+Pixelate the full image:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pixelate \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"blockSize": 20}'
+```
+
+Pixelate a specific region:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/pixelate \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"blockSize": 16, "region": {"left": 100, "top": 50, "width": 200, "height": 150}}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.jpg",
+ "originalSize": 2450000,
+ "processedSize": 2380000
+}
+```
+
+## Notes
+
+- When `region` is omitted, the entire image is pixelated.
+- The region coordinates are in pixels relative to the top-left corner of the image. The region must fall within the image bounds.
+- Output format matches the input format. HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/powerpoint-to-pdf.md b/apps/docs/tools/powerpoint-to-pdf.md
new file mode 100644
index 00000000..3b6d643f
--- /dev/null
+++ b/apps/docs/tools/powerpoint-to-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Convert presentations to PDF.
+---
+
+# PowerPoint to PDF
+
+Convert PowerPoint or OpenDocument presentations to PDF, with one slide per page.
+
+## API Endpoint
+
+`POST /api/v1/tools/powerpoint-to-pdf`
+
+Accepts multipart form data with a PowerPoint/ODP file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a presentation and it will be converted to PDF.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/powerpoint-to-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@slides.pptx"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.pptx`, `.ppt`, `.odp`.
+- Each slide becomes one page in the PDF.
+- Conversion is handled by LibreOffice running headless on the server.
+- Animations and transitions are not included in the PDF output.
diff --git a/apps/docs/tools/protect-pdf.md b/apps/docs/tools/protect-pdf.md
new file mode 100644
index 00000000..52dfe946
--- /dev/null
+++ b/apps/docs/tools/protect-pdf.md
@@ -0,0 +1,47 @@
+---
+description: Add password protection with AES-256 encryption to a PDF.
+---
+
+# Protect PDF
+
+Add password protection to a PDF using AES-256 encryption.
+
+## API Endpoint
+
+`POST /api/v1/tools/protect-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| userPassword | string | Yes | - | Password required to open the PDF (1-256 characters) |
+| ownerPassword | string | No | Same as `userPassword` | Owner password for permissions (1-256 characters) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/protect-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"userPassword": "s3cret", "ownerPassword": "0wn3r"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2500000
+}
+```
+
+## Notes
+
+- Encryption uses AES-256.
+- If `ownerPassword` is omitted, it defaults to the same value as `userPassword`.
+- Passwords are redacted from audit logs.
+- The encrypted PDF requires the user password to open and the owner password (if different) for full permissions.
diff --git a/apps/docs/tools/redact-pdf.md b/apps/docs/tools/redact-pdf.md
new file mode 100644
index 00000000..a449a3e4
--- /dev/null
+++ b/apps/docs/tools/redact-pdf.md
@@ -0,0 +1,49 @@
+---
+description: Permanently remove text occurrences from a PDF (verified true redaction).
+---
+
+# Redact PDF
+
+Permanently remove specified text occurrences from a PDF using verified true redaction. The redacted text is completely removed from the file, not just covered with a black box.
+
+## API Endpoint
+
+`POST /api/v1/tools/redact-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| terms | string[] | Yes | - | Text strings to redact (1-50 terms, each up to 200 characters) |
+| caseSensitive | boolean | No | `false` | Whether matching is case-sensitive |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/redact-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@contract.pdf" \
+ -F 'settings={"terms": ["John Doe", "555-0123"], "caseSensitive": false}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/contract.pdf",
+ "originalSize": 245000,
+ "processedSize": 243000,
+ "found": 7
+}
+```
+
+## Notes
+
+- Accepted input format: `.pdf`.
+- This is a fast (synchronous) tool that returns the result directly.
+- This performs true redaction: matched text is removed from the PDF content stream, not merely obscured visually.
+- The `found` field in the response indicates how many occurrences were redacted.
+- You can redact up to 50 terms in a single request.
diff --git a/apps/docs/tools/remove-pages.md b/apps/docs/tools/remove-pages.md
new file mode 100644
index 00000000..76b2dc5b
--- /dev/null
+++ b/apps/docs/tools/remove-pages.md
@@ -0,0 +1,44 @@
+---
+description: Delete specific pages from a PDF.
+---
+
+# Remove Pages
+
+Delete specific pages from a PDF, keeping all remaining pages intact.
+
+## API Endpoint
+
+`POST /api/v1/tools/remove-pages`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| pages | string | Yes | - | Page range to remove in qpdf syntax, e.g. `"3,5-7"` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/remove-pages \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"pages": "3,5-7"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 1800000
+}
+```
+
+## Notes
+
+- You cannot remove every page from a document; at least one page must remain.
+- Page ranges use qpdf syntax: `3` for a single page, `5-7` for a range, and commas to combine (e.g. `1,3,5-7`).
diff --git a/apps/docs/tools/repair-pdf.md b/apps/docs/tools/repair-pdf.md
new file mode 100644
index 00000000..c24e0d36
--- /dev/null
+++ b/apps/docs/tools/repair-pdf.md
@@ -0,0 +1,42 @@
+---
+description: Attempt to repair a damaged or corrupted PDF.
+---
+
+# Repair PDF
+
+Attempt to repair a damaged or corrupted PDF by reconstructing its internal structure.
+
+## API Endpoint
+
+`POST /api/v1/tools/repair-pdf`
+
+Accepts multipart form data with a PDF file. No `settings` field is required.
+
+## Parameters
+
+This tool has no settings parameters. Upload the damaged PDF file directly.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/repair-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@damaged.pdf"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/damaged.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2400000
+}
+```
+
+## Notes
+
+- Structural validation is skipped on input to allow malformed files through.
+- Repair is best-effort; severely corrupted files may not be fully recoverable.
+- The repaired PDF may differ slightly in size from the original due to reconstructed cross-reference tables.
diff --git a/apps/docs/tools/replace-audio.md b/apps/docs/tools/replace-audio.md
new file mode 100644
index 00000000..92ecb05e
--- /dev/null
+++ b/apps/docs/tools/replace-audio.md
@@ -0,0 +1,43 @@
+---
+description: Swap the audio track of a video with another file.
+---
+
+# Replace Audio
+
+Swap the audio track of a video with an audio file. Upload both a video and an audio file.
+
+## API Endpoint
+
+`POST /api/v1/tools/replace-audio`
+
+Accepts multipart form data with exactly two files: a video file followed by an audio file.
+
+## Parameters
+
+This tool has no settings parameters. Upload a video file and an audio file as two `file` parts.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/replace-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F "file=@voiceover.mp3"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 13100000
+}
+```
+
+## Notes
+
+- Exactly two files must be uploaded: the first must be a video, the second must be an audio file.
+- If the audio file is longer than the video, it is trimmed to match the video duration. If shorter, the remaining video plays in silence.
+- The video stream is copied without re-encoding, so there is no video quality loss.
diff --git a/apps/docs/tools/resize-video.md b/apps/docs/tools/resize-video.md
new file mode 100644
index 00000000..6c3ccd54
--- /dev/null
+++ b/apps/docs/tools/resize-video.md
@@ -0,0 +1,58 @@
+---
+description: Scale a video to a new resolution or preset size.
+---
+
+# Resize Video
+
+Scale a video to a new resolution using custom pixel dimensions or a standard preset.
+
+## API Endpoint
+
+`POST /api/v1/tools/resize-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| width | integer | No | - | Target width in pixels (16-7680) |
+| height | integer | No | - | Target height in pixels (16-4320) |
+| preset | string | No | `"custom"` | Resolution preset: `custom`, `2160p`, `1440p`, `1080p`, `720p`, `480p`, `360p` |
+
+When `preset` is `"custom"`, at least one of `width` or `height` must be provided. The other dimension scales proportionally.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/resize-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"preset": "720p"}'
+```
+
+Resize to custom dimensions:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/resize-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"width": 1280, "height": 720}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 25000000,
+ "processedSize": 8500000
+}
+```
+
+## Notes
+
+- Preset values map to standard heights (e.g. `720p` = 1280x720, `1080p` = 1920x1080). Width scales proportionally from the source aspect ratio.
+- Dimensions are rounded to even numbers as required by most video codecs.
+- Maximum supported resolution is 7680x4320 (8K UHD).
diff --git a/apps/docs/tools/reverse-audio.md b/apps/docs/tools/reverse-audio.md
new file mode 100644
index 00000000..44c24cb7
--- /dev/null
+++ b/apps/docs/tools/reverse-audio.md
@@ -0,0 +1,41 @@
+---
+description: Reverse an audio file so it plays backwards.
+---
+
+# Reverse Audio
+
+Reverse an audio file so it plays backwards.
+
+## API Endpoint
+
+`POST /api/v1/tools/reverse-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+This tool has no configurable parameters. The entire audio file is reversed.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/reverse-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- The full audio track is reversed from end to start.
+- Output format matches the input format.
diff --git a/apps/docs/tools/reverse-video.md b/apps/docs/tools/reverse-video.md
new file mode 100644
index 00000000..0c428332
--- /dev/null
+++ b/apps/docs/tools/reverse-video.md
@@ -0,0 +1,41 @@
+---
+description: Play a video clip backwards.
+---
+
+# Reverse Video
+
+Play a video clip backwards. The audio track is also reversed.
+
+## API Endpoint
+
+`POST /api/v1/tools/reverse-video`
+
+Accepts multipart form data with a video file. This tool has no configurable settings.
+
+## Parameters
+
+This tool has no parameters. It reverses the entire video.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/reverse-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12600000
+}
+```
+
+## Notes
+
+- Limited to clips up to 5 minutes in length. Longer videos are rejected with a 400 error.
+- Both video and audio tracks are reversed. To reverse video without audio, mute it first.
diff --git a/apps/docs/tools/ringtone-maker.md b/apps/docs/tools/ringtone-maker.md
new file mode 100644
index 00000000..5d8f276a
--- /dev/null
+++ b/apps/docs/tools/ringtone-maker.md
@@ -0,0 +1,46 @@
+---
+description: Create a ringtone clip from any audio file.
+---
+
+# Ringtone Maker
+
+Create a ringtone clip (.m4r) from any audio file by selecting a start time and duration.
+
+## API Endpoint
+
+`POST /api/v1/tools/ringtone-maker`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| startS | number | No | `0` | Start time in seconds (minimum 0) |
+| durationS | number | No | `30` | Clip duration in seconds (1 to 30) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/ringtone-maker \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"startS": 15, "durationS": 20}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.m4r",
+ "originalSize": 4500000,
+ "processedSize": 620000
+}
+```
+
+## Notes
+
+- Output is always M4R format, compatible with iPhone ringtones.
+- Maximum ringtone duration is 30 seconds (Apple limit).
+- Any audio format can be used as input.
diff --git a/apps/docs/tools/rotate-pdf.md b/apps/docs/tools/rotate-pdf.md
new file mode 100644
index 00000000..a42d6677
--- /dev/null
+++ b/apps/docs/tools/rotate-pdf.md
@@ -0,0 +1,46 @@
+---
+description: Rotate pages in a PDF by 90, 180, or 270 degrees.
+---
+
+# Rotate PDF
+
+Rotate all or selected pages in a PDF by a specified angle.
+
+## API Endpoint
+
+`POST /api/v1/tools/rotate-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| angle | integer | No | `90` | Rotation angle: `90`, `180`, or `270` |
+| range | string | No | `"1-z"` | Page range in qpdf syntax, e.g. `"1-5,8"` (`"1-z"` = all pages) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/rotate-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"angle": 90, "range": "1-3"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2450000
+}
+```
+
+## Notes
+
+- Rotation is clockwise.
+- Page ranges use qpdf syntax: `1-5` for pages 1 through 5, `z` for the last page, and commas to combine ranges.
+- The default range `"1-z"` rotates all pages.
diff --git a/apps/docs/tools/rotate-video.md b/apps/docs/tools/rotate-video.md
new file mode 100644
index 00000000..9d9a827f
--- /dev/null
+++ b/apps/docs/tools/rotate-video.md
@@ -0,0 +1,52 @@
+---
+description: Rotate or flip a video.
+---
+
+# Rotate Video
+
+Rotate a video by 90, 180, or 270 degrees, or flip it horizontally or vertically.
+
+## API Endpoint
+
+`POST /api/v1/tools/rotate-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| transform | string | Yes | - | Transformation to apply: `cw90`, `ccw90`, `180`, `hflip`, `vflip` |
+
+### Transform Values
+
+- **cw90** - Rotate 90 degrees clockwise
+- **ccw90** - Rotate 90 degrees counter-clockwise
+- **180** - Rotate 180 degrees
+- **hflip** - Flip horizontally (mirror)
+- **vflip** - Flip vertically
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/rotate-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"transform": "cw90"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12400000
+}
+```
+
+## Notes
+
+- Rotations by 90 or 270 degrees swap the video's width and height.
+- Flip operations (hflip, vflip) do not change the video dimensions.
diff --git a/apps/docs/tools/silence-removal.md b/apps/docs/tools/silence-removal.md
new file mode 100644
index 00000000..9734bbe6
--- /dev/null
+++ b/apps/docs/tools/silence-removal.md
@@ -0,0 +1,47 @@
+---
+description: Strip silent sections from an audio file.
+---
+
+# Silence Removal
+
+Detect and remove silent sections from an audio file based on a configurable threshold and minimum duration.
+
+## API Endpoint
+
+`POST /api/v1/tools/silence-removal`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| thresholdDb | number | No | `-50` | Silence threshold in dB (-80 to -20). Audio below this level is considered silent. |
+| minSilenceS | number | No | `0.5` | Minimum silence duration in seconds to remove (0.1 to 5) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/silence-removal \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"thresholdDb": -45, "minSilenceS": 1}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 3200000
+}
+```
+
+## Notes
+
+- A higher (less negative) threshold is more aggressive and removes quieter passages as well as true silence.
+- Increase `minSilenceS` to only strip longer pauses while keeping short natural gaps.
+- Useful for cleaning up podcast recordings, lectures, and voice memos.
+- Output format matches the input format.
diff --git a/apps/docs/tools/split-audio.md b/apps/docs/tools/split-audio.md
new file mode 100644
index 00000000..9bd2bfff
--- /dev/null
+++ b/apps/docs/tools/split-audio.md
@@ -0,0 +1,61 @@
+---
+description: Split audio by time intervals, equal parts, or silence detection.
+---
+
+# Split Audio
+
+Split an audio file into segments by fixed time intervals, equal parts, or automatic silence detection. Returns a ZIP archive of the segments.
+
+## API Endpoint
+
+`POST /api/v1/tools/split-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| mode | string | No | `"time"` | Split strategy: `time`, `parts`, `silence` |
+| segmentS | number | No | `60` | Segment length in seconds, 1 to 3600 (used when mode is `time`) |
+| parts | integer | No | `2` | Number of equal parts, 2 to 20 (used when mode is `parts`) |
+| thresholdDb | number | No | `-40` | Silence threshold in dB, -80 to -20 (used when mode is `silence`) |
+| minSilenceS | number | No | `0.3` | Minimum silence gap in seconds, 0.1 to 10 (used when mode is `silence`) |
+
+## Example Request
+
+Split into 30-second segments:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/split-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"mode": "time", "segmentS": 30}'
+```
+
+Split by silence detection:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/split-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"mode": "silence", "thresholdDb": -35, "minSilenceS": 0.5}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio-segments.zip",
+ "originalSize": 4500000,
+ "processedSize": 4600000
+}
+```
+
+## Notes
+
+- The `downloadUrl` points to a ZIP archive containing all segments.
+- Only the parameters relevant to the chosen `mode` are used; others are ignored.
+- Segment filenames are numbered sequentially (e.g. `001.mp3`, `002.mp3`).
+- Output format matches the input format.
diff --git a/apps/docs/tools/split-csv.md b/apps/docs/tools/split-csv.md
new file mode 100644
index 00000000..6c310ea7
--- /dev/null
+++ b/apps/docs/tools/split-csv.md
@@ -0,0 +1,47 @@
+---
+description: Split a CSV into smaller files by row count.
+---
+
+# Split CSV
+
+Split a large CSV or TSV file into smaller files by row count. Returns a ZIP archive containing the parts.
+
+## API Endpoint
+
+`POST /api/v1/tools/split-csv`
+
+Accepts multipart form data with a CSV file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| rowsPerFile | integer | No | `1000` | Number of data rows per output file (1--1,000,000) |
+| keepHeader | boolean | No | `true` | Repeat the header row in each output file |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/split-csv \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@large-dataset.csv" \
+ -F 'settings={"rowsPerFile": 500, "keepHeader": true}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/large-dataset-split.zip",
+ "originalSize": 1048576,
+ "processedSize": 1050000
+}
+```
+
+## Notes
+
+- Output is always a ZIP archive containing the split CSV parts, named sequentially (e.g. `part-1.csv`, `part-2.csv`).
+- When `keepHeader` is `true`, each part includes the original header row so each file can be used independently.
+- Both CSV and TSV files are accepted as input.
+- The row count refers to data rows only; the header row is not counted.
diff --git a/apps/docs/tools/split-pdf.md b/apps/docs/tools/split-pdf.md
new file mode 100644
index 00000000..2d269c56
--- /dev/null
+++ b/apps/docs/tools/split-pdf.md
@@ -0,0 +1,58 @@
+---
+description: Extract pages or split a PDF into parts.
+---
+
+# Split PDF
+
+Extract a range of pages into a new PDF, or split a document into chunks of N pages.
+
+## API Endpoint
+
+`POST /api/v1/tools/split-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| mode | string | No | `"range"` | Split mode: `range` or `every` |
+| range | string | When mode is `range` | - | Page range in qpdf syntax, e.g. `"1-5,8,10-z"` |
+| everyN | integer | When mode is `every` | - | Split into chunks of N pages (1-500) |
+
+## Example Request
+
+Extract specific pages:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/split-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"mode": "range", "range": "1-5,8"}'
+```
+
+Split into chunks of 10 pages:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/split-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"mode": "every", "everyN": 10}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 980000
+}
+```
+
+## Notes
+
+- In `range` mode, a single PDF containing the selected pages is returned.
+- In `every` mode, the result is a ZIP archive containing the individual parts.
+- Page ranges use qpdf syntax: `1-5` for pages 1 through 5, `z` for the last page, and commas to combine ranges (e.g. `1-3,7,10-z`).
diff --git a/apps/docs/tools/sprite-sheet.md b/apps/docs/tools/sprite-sheet.md
new file mode 100644
index 00000000..cbbbb4b5
--- /dev/null
+++ b/apps/docs/tools/sprite-sheet.md
@@ -0,0 +1,66 @@
+---
+description: Combine multiple images into a single sprite sheet grid with frame metadata.
+---
+
+# Sprite Sheet
+
+Combine multiple images into a single sprite sheet grid. Each image is resized to match the first image's dimensions and placed into the grid. Returns the sprite sheet image along with per-frame coordinate metadata.
+
+## API Endpoint
+
+`POST /api/v1/tools/sprite-sheet`
+
+Accepts multipart form data with two or more image files and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| columns | integer | No | `4` | Number of columns in the grid (1-16) |
+| padding | integer | No | `0` | Padding between cells in pixels (0-64) |
+| background | string | No | `"#ffffff"` | Background hex color |
+| format | string | No | `"png"` | Output format: `png`, `webp`, or `jpeg` |
+| quality | integer | No | `90` | Output quality (1-100) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/sprite-sheet \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@frame1.png" \
+ -F "file=@frame2.png" \
+ -F "file=@frame3.png" \
+ -F "file=@frame4.png" \
+ -F 'settings={"columns": 2, "padding": 4, "format": "png"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/sprite-sheet.png",
+ "originalSize": 120000,
+ "processedSize": 95000,
+ "frames": [
+ { "index": 0, "left": 0, "top": 0, "width": 128, "height": 128 },
+ { "index": 1, "left": 132, "top": 0, "width": 128, "height": 128 },
+ { "index": 2, "left": 0, "top": 132, "width": 128, "height": 128 },
+ { "index": 3, "left": 132, "top": 132, "width": 128, "height": 128 }
+ ],
+ "cols": 2,
+ "rows": 2,
+ "cellWidth": 128,
+ "cellHeight": 128,
+ "canvasWidth": 260,
+ "canvasHeight": 260
+}
+```
+
+## Notes
+
+- Accepts 2 to 64 images. All images are resized to match the dimensions of the first uploaded image.
+- The `frames` array provides the exact pixel coordinates of each frame in the output, suitable for CSS sprite definitions or game engine frame maps.
+- The number of rows is calculated automatically from the image count and `columns` value.
+- Use the `padding` parameter to add spacing between cells. The `background` color is visible in padding areas and any empty trailing cells.
+- HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/stabilize-video.md b/apps/docs/tools/stabilize-video.md
new file mode 100644
index 00000000..9e748698
--- /dev/null
+++ b/apps/docs/tools/stabilize-video.md
@@ -0,0 +1,43 @@
+---
+description: Reduce camera shake with two-pass stabilization.
+---
+
+# Stabilize Video
+
+Reduce camera shake in handheld footage using FFmpeg's two-pass vidstab stabilization.
+
+## API Endpoint
+
+`POST /api/v1/tools/stabilize-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| smoothing | integer | No | `15` | Smoothing window size in frames (5-60). Higher values produce smoother motion |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/stabilize-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"smoothing": 30}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Stabilization is a two-pass process: the first pass analyzes camera motion, and the second pass applies the correction. This takes roughly twice as long as single-pass tools.
+- Higher smoothing values remove more shake but may introduce a slight zoom crop at the edges.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/to-epub.md b/apps/docs/tools/to-epub.md
new file mode 100644
index 00000000..a42e0593
--- /dev/null
+++ b/apps/docs/tools/to-epub.md
@@ -0,0 +1,43 @@
+---
+description: Convert Word, Markdown, HTML, or plain text files to EPUB.
+---
+
+# Convert to EPUB
+
+Convert Word documents, Markdown, HTML, or plain text files into the EPUB e-book format.
+
+## API Endpoint
+
+`POST /api/v1/tools/to-epub`
+
+Accepts multipart form data with a Word/Markdown/HTML/TXT file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a document and it will be converted to EPUB.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/to-epub \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@manuscript.docx"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.docx`, `.md`, `.html`, `.txt`.
+- The EPUB output follows the EPUB 3 specification.
+- Headings in the source document are used to generate the table of contents.
+- Conversion is handled by Pandoc on the server.
diff --git a/apps/docs/tools/transcribe-audio.md b/apps/docs/tools/transcribe-audio.md
new file mode 100644
index 00000000..44269dc5
--- /dev/null
+++ b/apps/docs/tools/transcribe-audio.md
@@ -0,0 +1,50 @@
+---
+description: Convert speech to text with AI-powered transcription.
+---
+
+# Transcribe Audio
+
+Convert speech to text using AI-powered transcription (faster-whisper). Supports plain text, SRT, and VTT output formats with automatic or manual language selection.
+
+## API Endpoint
+
+`POST /api/v1/tools/transcribe-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| language | string | No | `"auto"` | Language: `auto`, `en`, `de`, `fr`, `es`, `zh`, `ja`, `ko`, `id`, `th`, `vi` |
+| outputFormat | string | No | `"txt"` | Output format: `txt`, `srt`, `vtt` |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/transcribe-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"language": "en", "outputFormat": "srt"}'
+```
+
+## Example Response
+
+This is an async tool. The API returns `202 Accepted` immediately:
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+Track progress via SSE at `GET /api/v1/jobs/{jobId}/progress`. When the job completes, the SSE stream delivers the final result with a `downloadUrl`.
+
+## Notes
+
+- Requires the **transcription** feature bundle to be installed. Returns `501 Not Implemented` if the bundle is not available.
+- Uses faster-whisper for transcription. Language `auto` detects the spoken language automatically.
+- `srt` and `vtt` formats include timestamps for each segment, suitable for subtitles.
+- `txt` format returns plain text without timestamps.
+- This is a long-running AI tool; processing time depends on audio length and server hardware.
diff --git a/apps/docs/tools/trim-audio.md b/apps/docs/tools/trim-audio.md
new file mode 100644
index 00000000..0cc4d0b3
--- /dev/null
+++ b/apps/docs/tools/trim-audio.md
@@ -0,0 +1,47 @@
+---
+description: Cut a section out of an audio file by specifying start and end times.
+---
+
+# Trim Audio
+
+Cut a section out of an audio file by specifying start and end times in seconds.
+
+## API Endpoint
+
+`POST /api/v1/tools/trim-audio`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| startS | number | No | `0` | Start time in seconds (minimum 0) |
+| endS | number | Yes | - | End time in seconds (must be after start) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/trim-audio \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"startS": 10, "endS": 45}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 1575000
+}
+```
+
+## Notes
+
+- Times are specified in seconds and can include decimals (e.g. `10.5`).
+- The `endS` value must be greater than `startS`.
+- If `endS` exceeds the audio duration, the file is trimmed to the end.
+- Output format matches the input format.
diff --git a/apps/docs/tools/trim-video.md b/apps/docs/tools/trim-video.md
new file mode 100644
index 00000000..1ef71893
--- /dev/null
+++ b/apps/docs/tools/trim-video.md
@@ -0,0 +1,47 @@
+---
+description: Cut a clip out of a video by specifying start and end times.
+---
+
+# Trim Video
+
+Cut a clip out of a video by specifying start and end times in seconds, with an option for frame-accurate cuts.
+
+## API Endpoint
+
+`POST /api/v1/tools/trim-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| startS | number | No | `0` | Start time in seconds (must be >= 0) |
+| endS | number | Yes | - | End time in seconds (must be after startS) |
+| precise | boolean | No | `false` | Re-encode for frame-accurate cuts instead of keyframe seek |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/trim-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"startS": 5, "endS": 30, "precise": true}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 4200000
+}
+```
+
+## Notes
+
+- When `precise` is `false` (the default), the tool uses keyframe seeking, which is fast but may start a few frames before the requested time.
+- Setting `precise` to `true` re-encodes the segment for exact frame boundaries, but takes longer.
+- The `endS` value must be greater than `startS`.
diff --git a/apps/docs/tools/unlock-pdf.md b/apps/docs/tools/unlock-pdf.md
new file mode 100644
index 00000000..22435175
--- /dev/null
+++ b/apps/docs/tools/unlock-pdf.md
@@ -0,0 +1,45 @@
+---
+description: Remove password protection from a PDF.
+---
+
+# Unlock PDF
+
+Remove password protection from an encrypted PDF by providing the correct password.
+
+## API Endpoint
+
+`POST /api/v1/tools/unlock-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| password | string | Yes | - | Password to decrypt the PDF (1-256 characters) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/unlock-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"password": "s3cret"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2500000,
+ "processedSize": 2450000
+}
+```
+
+## Notes
+
+- The correct password must be provided; an incorrect password returns a 400 error.
+- Either the user password or the owner password will work for decryption.
+- Passwords are redacted from audit logs.
diff --git a/apps/docs/tools/video-color.md b/apps/docs/tools/video-color.md
new file mode 100644
index 00000000..9fb11f94
--- /dev/null
+++ b/apps/docs/tools/video-color.md
@@ -0,0 +1,48 @@
+---
+description: Adjust brightness, contrast, saturation, and gamma of a video.
+---
+
+# Video Color
+
+Adjust brightness, contrast, saturation, and gamma correction on a video.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-color`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| brightness | number | No | `0` | Brightness adjustment (-1 to 1) |
+| contrast | number | No | `1` | Contrast multiplier (0-4) |
+| saturation | number | No | `1` | Saturation multiplier (0-3). Set to 0 for grayscale |
+| gamma | number | No | `1` | Gamma correction (0.1-10) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-color \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"brightness": 0.1, "contrast": 1.2, "saturation": 1.5}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12300000
+}
+```
+
+## Notes
+
+- All values at their defaults (brightness 0, contrast 1, saturation 1, gamma 1) produce no change.
+- Setting saturation to `0` converts the video to grayscale.
+- Gamma values below 1 brighten shadows, while values above 1 darken them.
diff --git a/apps/docs/tools/video-loudnorm.md b/apps/docs/tools/video-loudnorm.md
new file mode 100644
index 00000000..b532987d
--- /dev/null
+++ b/apps/docs/tools/video-loudnorm.md
@@ -0,0 +1,42 @@
+---
+description: Normalize video audio volume to broadcast standard.
+---
+
+# Normalize Audio
+
+Normalize video audio volume to the EBU R128 broadcast loudness standard.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-loudnorm`
+
+Accepts multipart form data with a video file. This tool has no configurable settings.
+
+## Parameters
+
+This tool has no parameters. It applies EBU R128 loudness normalization to the audio track.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-loudnorm \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12500000
+}
+```
+
+## Notes
+
+- Uses FFmpeg's two-pass `loudnorm` filter targeting -14 LUFS integrated loudness, the standard for streaming platforms.
+- The source audio sample rate is preserved in the output.
+- If the video has no audio track, the request returns a 400 error.
diff --git a/apps/docs/tools/video-metadata.md b/apps/docs/tools/video-metadata.md
new file mode 100644
index 00000000..760cd84c
--- /dev/null
+++ b/apps/docs/tools/video-metadata.md
@@ -0,0 +1,42 @@
+---
+description: Strip metadata from a video and report what was found.
+---
+
+# Clean Video Metadata
+
+Strip metadata (creation date, GPS coordinates, camera model, software tags, etc.) from a video and report what was removed.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-metadata`
+
+Accepts multipart form data with a video file. This tool has no configurable settings.
+
+## Parameters
+
+This tool has no parameters. It strips all metadata from the video container.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-metadata \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12480000
+}
+```
+
+## Notes
+
+- Metadata stripped includes creation timestamps, GPS/location data, camera/device info, and software tags.
+- The video and audio streams are copied without re-encoding, so there is no quality loss.
+- Useful for privacy before sharing videos publicly.
diff --git a/apps/docs/tools/video-speed.md b/apps/docs/tools/video-speed.md
new file mode 100644
index 00000000..a9022072
--- /dev/null
+++ b/apps/docs/tools/video-speed.md
@@ -0,0 +1,46 @@
+---
+description: Speed up or slow down a video.
+---
+
+# Video Speed
+
+Speed up or slow down a video with an option to preserve audio pitch.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-speed`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| factor | number | No | `2` | Speed multiplier (0.25-4). Values above 1 speed up, below 1 slow down |
+| keepPitch | boolean | No | `true` | Preserve audio pitch when changing speed |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-speed \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"factor": 0.5, "keepPitch": true}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 24800000
+}
+```
+
+## Notes
+
+- A factor of `2` doubles playback speed (halves duration). A factor of `0.5` halves playback speed (doubles duration).
+- When `keepPitch` is `true`, the audio is time-stretched so voices sound natural. When `false`, pitch shifts proportionally with speed.
+- The valid range is 0.25x to 4x.
diff --git a/apps/docs/tools/video-to-frames.md b/apps/docs/tools/video-to-frames.md
new file mode 100644
index 00000000..bfc431ab
--- /dev/null
+++ b/apps/docs/tools/video-to-frames.md
@@ -0,0 +1,59 @@
+---
+description: Extract frames from a video as a ZIP of images.
+---
+
+# Video to Frames
+
+Extract individual frames from a video and download them as a ZIP archive of PNG or JPG images.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-to-frames`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| mode | string | No | `"all"` | Extraction mode: `all`, `nth`, `timestamps` |
+| n | integer | No | `10` | Extract every Nth frame (2-1000). Only used when mode is `"nth"` |
+| timestamps | string | No | `""` | Comma-separated timestamps in seconds. Required when mode is `"timestamps"` |
+| format | string | No | `"png"` | Image format for extracted frames: `png`, `jpg` |
+
+## Example Request
+
+Extract every 30th frame as JPG:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-to-frames \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"mode": "nth", "n": 30, "format": "jpg"}'
+```
+
+Extract frames at specific timestamps:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-to-frames \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"mode": "timestamps", "timestamps": "1.5,5,12.3"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip-frames.zip",
+ "originalSize": 12500000,
+ "processedSize": 45000000
+}
+```
+
+## Notes
+
+- The `all` mode extracts every frame and can produce very large ZIP files for long videos. Use `nth` or `timestamps` mode for selective extraction.
+- PNG preserves full quality but produces larger files. JPG is smaller but lossy.
+- The response downloads as a ZIP archive containing sequentially numbered image files.
diff --git a/apps/docs/tools/video-to-gif.md b/apps/docs/tools/video-to-gif.md
new file mode 100644
index 00000000..6ddf37db
--- /dev/null
+++ b/apps/docs/tools/video-to-gif.md
@@ -0,0 +1,46 @@
+---
+description: Turn a video clip into an animated GIF.
+---
+
+# Video to GIF
+
+Turn a video clip into an animated GIF with configurable frame rate, width, start time, and duration.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-to-gif`
+
+Accepts multipart form data with a video file and a JSON `settings` field. This is an async endpoint - it returns `202 Accepted` immediately and progress is streamed via SSE at `GET /api/v1/jobs/{jobId}/progress`.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| fps | integer | No | `12` | Output frame rate (1-30) |
+| width | integer | No | `480` | Output width in pixels (64-1280). Height scales proportionally |
+| startS | number | No | `0` | Start time in seconds (must be >= 0) |
+| durationS | number | No | `5` | Duration in seconds (0-60) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-to-gif \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"fps": 15, "width": 320, "startS": 2, "durationS": 8}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Lower `fps` and `width` values produce smaller GIF files. A 480px-wide GIF at 12 fps is usually a good balance.
+- Maximum duration is 60 seconds. Longer clips produce very large files.
+- Progress updates are available via SSE at `GET /api/v1/jobs/{jobId}/progress` until the job completes.
diff --git a/apps/docs/tools/video-to-webp.md b/apps/docs/tools/video-to-webp.md
new file mode 100644
index 00000000..895d9659
--- /dev/null
+++ b/apps/docs/tools/video-to-webp.md
@@ -0,0 +1,48 @@
+---
+description: Convert a video clip into an animated WebP image.
+---
+
+# Video to WebP
+
+Convert a video clip into an animated WebP image with configurable frame rate, width, and quality.
+
+## API Endpoint
+
+`POST /api/v1/tools/video-to-webp`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| fps | integer | No | `12` | Output frame rate (1-30) |
+| width | integer | No | `480` | Output width in pixels (16-1920). Height scales proportionally |
+| quality | integer | No | `75` | WebP compression quality (1-100) |
+| loop | boolean | No | `true` | Loop the animation |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/video-to-webp \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"fps": 15, "width": 640, "quality": 80}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.webp",
+ "originalSize": 12500000,
+ "processedSize": 2800000
+}
+```
+
+## Notes
+
+- Animated WebP produces smaller files than GIF with better color support (24-bit vs 8-bit palette).
+- Lower `quality` values produce smaller files at the cost of visual fidelity.
+- Set `loop` to `false` for animations that should play once and stop.
diff --git a/apps/docs/tools/vignette.md b/apps/docs/tools/vignette.md
new file mode 100644
index 00000000..e6576c61
--- /dev/null
+++ b/apps/docs/tools/vignette.md
@@ -0,0 +1,52 @@
+---
+description: Add a vignette effect with adjustable strength, color, and position.
+---
+
+# Vignette
+
+Add a vignette effect that darkens or tints the edges of an image. Supports adjustable strength, color, radius, softness, roundness, and center position.
+
+## API Endpoint
+
+`POST /api/v1/tools/vignette`
+
+Accepts multipart form data with an image file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| strength | number | No | `0.5` | Vignette opacity (0.1-1) |
+| color | string | No | `"#000000"` | Vignette hex color |
+| radius | integer | No | `70` | Outer radius as percentage of half-diagonal (0-100) |
+| softness | integer | No | `50` | Feather softness (0-100); higher values produce a more gradual fade |
+| roundness | integer | No | `100` | Shape: 100 = circle, 0 = ellipse matching image aspect ratio |
+| centerX | integer | No | `50` | Horizontal center position as percentage (0-100) |
+| centerY | integer | No | `50` | Vertical center position as percentage (0-100) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/vignette \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@photo.jpg" \
+ -F 'settings={"strength": 0.7, "radius": 60, "softness": 70}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/photo.jpg",
+ "originalSize": 2450000,
+ "processedSize": 2410000
+}
+```
+
+## Notes
+
+- A smaller `radius` darkens more of the image; a larger radius confines the vignette to the extreme edges.
+- Use a non-black `color` (e.g., white or sepia tones) for creative vignette effects.
+- Adjusting `centerX` and `centerY` lets you position the clear area off-center, useful for drawing focus to a subject that is not in the middle of the frame.
+- Output format matches the input format. HEIC, RAW, PSD, and SVG inputs are automatically decoded before processing.
diff --git a/apps/docs/tools/volume-adjust.md b/apps/docs/tools/volume-adjust.md
new file mode 100644
index 00000000..97c1f201
--- /dev/null
+++ b/apps/docs/tools/volume-adjust.md
@@ -0,0 +1,45 @@
+---
+description: Increase or decrease audio volume by a fixed gain in decibels.
+---
+
+# Volume Adjust
+
+Increase or decrease the volume of an audio file by applying a fixed gain in decibels.
+
+## API Endpoint
+
+`POST /api/v1/tools/volume-adjust`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| gainDb | number | No | `3` | Volume adjustment in decibels (-30 to 30) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/volume-adjust \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"gainDb": 6}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.mp3",
+ "originalSize": 4500000,
+ "processedSize": 4500000
+}
+```
+
+## Notes
+
+- Positive values increase volume; negative values decrease it.
+- Large positive gains can cause clipping. Use normalize-audio for loudness-safe leveling.
+- Output format matches the input format.
diff --git a/apps/docs/tools/watermark-pdf.md b/apps/docs/tools/watermark-pdf.md
new file mode 100644
index 00000000..bbb6009f
--- /dev/null
+++ b/apps/docs/tools/watermark-pdf.md
@@ -0,0 +1,55 @@
+---
+description: Add a text watermark to every page of a PDF.
+---
+
+# Watermark PDF
+
+Stamp a text watermark on every page of a PDF with configurable position, size, opacity, and rotation.
+
+## API Endpoint
+
+`POST /api/v1/tools/watermark-pdf`
+
+Accepts multipart form data with a PDF file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| text | string | Yes | - | Watermark text (1-200 characters) |
+| position | string | No | `"c"` | Placement on the page: `tl`, `tc`, `tr`, `l`, `c`, `r`, `bl`, `bc`, `br` |
+| fontSize | integer | No | `48` | Font size in points (6-72) |
+| opacity | number | No | `0.3` | Watermark opacity (0.05-1) |
+| rotation | number | No | `45` | Rotation angle in degrees (-180 to 180) |
+
+### Position Values
+
+- `tl` top-left, `tc` top-center, `tr` top-right
+- `l` center-left, `c` center, `r` center-right
+- `bl` bottom-left, `bc` bottom-center, `br` bottom-right
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/watermark-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@document.pdf" \
+ -F 'settings={"text": "CONFIDENTIAL", "position": "c", "opacity": 0.2, "rotation": 45}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/document.pdf",
+ "originalSize": 2450000,
+ "processedSize": 2500000
+}
+```
+
+## Notes
+
+- The watermark is rendered as a text overlay on each page.
+- The same watermark text, position, and style are applied uniformly to all pages.
+- Use lower opacity values (0.1-0.3) for subtle watermarks that do not obscure content.
diff --git a/apps/docs/tools/watermark-video.md b/apps/docs/tools/watermark-video.md
new file mode 100644
index 00000000..13ca28c4
--- /dev/null
+++ b/apps/docs/tools/watermark-video.md
@@ -0,0 +1,55 @@
+---
+description: Burn a text watermark onto video frames.
+---
+
+# Watermark Video
+
+Burn a text watermark onto every frame of a video with configurable position, size, opacity, and color.
+
+## API Endpoint
+
+`POST /api/v1/tools/watermark-video`
+
+Accepts multipart form data with a video file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| text | string | Yes | - | Watermark text (1-200 characters) |
+| position | string | No | `"br"` | Position on the frame: `tl`, `tc`, `tr`, `l`, `c`, `r`, `bl`, `bc`, `br` |
+| fontSize | integer | No | `36` | Font size in pixels (8-120) |
+| opacity | number | No | `0.5` | Watermark opacity (0.05-1) |
+| color | string | No | `"#ffffff"` | Hex color for the text (e.g. `"#ffffff"`) |
+
+### Position Values
+
+- **tl** - Top left, **tc** - Top center, **tr** - Top right
+- **l** - Middle left, **c** - Center, **r** - Middle right
+- **bl** - Bottom left, **bc** - Bottom center, **br** - Bottom right
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/watermark-video \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@clip.mp4" \
+ -F 'settings={"text": "PREVIEW", "position": "c", "fontSize": 48, "opacity": 0.3}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/clip.mp4",
+ "originalSize": 12500000,
+ "processedSize": 12400000
+}
+```
+
+## Notes
+
+- The watermark is permanently rendered into the video frames and cannot be removed after processing.
+- The watermark uses a sans-serif font built into FFmpeg.
+- For image watermarks, use the image Watermark tool instead.
diff --git a/apps/docs/tools/waveform-image.md b/apps/docs/tools/waveform-image.md
new file mode 100644
index 00000000..bb269c2d
--- /dev/null
+++ b/apps/docs/tools/waveform-image.md
@@ -0,0 +1,47 @@
+---
+description: Generate a waveform visualization as a PNG image from an audio file.
+---
+
+# Waveform Image
+
+Generate a waveform visualization as a PNG image from an audio file, with configurable dimensions and color.
+
+## API Endpoint
+
+`POST /api/v1/tools/waveform-image`
+
+Accepts multipart form data with an audio file and a JSON `settings` field.
+
+## Parameters
+
+| Parameter | Type | Required | Default | Description |
+|-----------|------|----------|---------|-------------|
+| width | integer | No | `1024` | Image width in pixels (256 to 3840) |
+| height | integer | No | `256` | Image height in pixels (64 to 1080) |
+| color | string | No | `"#4f46e5"` | Waveform hex color (e.g. `"#4f46e5"`) |
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/waveform-image \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@audio.mp3" \
+ -F 'settings={"width": 1920, "height": 400, "color": "#e07832"}'
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/audio.png",
+ "originalSize": 4500000,
+ "processedSize": 45000
+}
+```
+
+## Notes
+
+- The output is always a PNG image, regardless of the input audio format.
+- The waveform is rendered on a transparent background.
+- Useful for thumbnails, social media previews, or embedding in web pages.
diff --git a/apps/docs/tools/word-to-pdf.md b/apps/docs/tools/word-to-pdf.md
new file mode 100644
index 00000000..ea7dead8
--- /dev/null
+++ b/apps/docs/tools/word-to-pdf.md
@@ -0,0 +1,43 @@
+---
+description: Convert Word documents to PDF.
+---
+
+# Word to PDF
+
+Convert Word documents, OpenDocument text, RTF, or plain text files to PDF.
+
+## API Endpoint
+
+`POST /api/v1/tools/word-to-pdf`
+
+Accepts multipart form data with a Word/ODT/RTF/TXT file.
+
+## Parameters
+
+This tool has no configurable parameters. Upload a document and it will be converted to PDF.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/word-to-pdf \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@report.docx"
+```
+
+## Example Response
+
+Returns `202 Accepted`. Track progress via SSE at `/api/v1/jobs/{jobId}/progress`.
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "async": true
+}
+```
+
+## Notes
+
+- Accepted input formats: `.docx`, `.doc`, `.odt`, `.rtf`, `.txt`.
+- Conversion is handled by LibreOffice running headless on the server.
+- Fonts embedded in the document are used when available; otherwise system fonts are substituted.
+- Headers, footers, tables, and images are preserved in the PDF output.
diff --git a/apps/docs/tools/xml-to-csv.md b/apps/docs/tools/xml-to-csv.md
new file mode 100644
index 00000000..39136165
--- /dev/null
+++ b/apps/docs/tools/xml-to-csv.md
@@ -0,0 +1,43 @@
+---
+description: Extract repeating elements from XML into a CSV table.
+---
+
+# XML to CSV
+
+Extract repeating elements from an XML file into a flat CSV table. The tool automatically finds the first array of objects in the XML tree and maps each element to a row.
+
+## API Endpoint
+
+`POST /api/v1/tools/xml-to-csv`
+
+Accepts multipart form data with an XML file. No settings field is required.
+
+## Parameters
+
+This tool has no configurable parameters. The repeating element is auto-detected from the XML structure.
+
+## Example Request
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/xml-to-csv \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@catalog.xml"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/catalog.csv",
+ "originalSize": 4500,
+ "processedSize": 1800
+}
+```
+
+## Notes
+
+- Only `.xml` files are accepted as input.
+- The tool scans the XML tree for the first repeating set of sibling elements and uses those as rows.
+- Each unique child element or attribute name becomes a CSV column header.
+- This is a one-way conversion. For bidirectional JSON/XML conversion, use the [JSON to XML](/tools/json-xml) tool.
diff --git a/apps/docs/tools/yaml-json.md b/apps/docs/tools/yaml-json.md
new file mode 100644
index 00000000..a8607225
--- /dev/null
+++ b/apps/docs/tools/yaml-json.md
@@ -0,0 +1,52 @@
+---
+description: Convert between YAML and JSON, both directions.
+---
+
+# YAML / JSON
+
+Convert between YAML and JSON formats in both directions. Upload a YAML file to get JSON, or upload a JSON file to get YAML.
+
+## API Endpoint
+
+`POST /api/v1/tools/yaml-json`
+
+Accepts multipart form data with a YAML or JSON file. No settings field is required.
+
+## Parameters
+
+This tool has no configurable parameters. The conversion direction is determined by the input file extension.
+
+## Example Request
+
+YAML to JSON:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/yaml-json \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@config.yaml"
+```
+
+JSON to YAML:
+
+```bash
+curl -X POST http://localhost:1349/api/v1/tools/yaml-json \
+ -H "Authorization: Bearer si_your-api-key" \
+ -F "file=@config.json"
+```
+
+## Example Response
+
+```json
+{
+ "jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "downloadUrl": "/api/v1/download/a1b2c3d4-e5f6-7890-abcd-ef1234567890/config.json",
+ "originalSize": 620,
+ "processedSize": 780
+}
+```
+
+## Notes
+
+- Conversion direction is auto-detected from the input file extension: `.yaml` or `.yml` produces `.json`, and `.json` produces `.yaml`.
+- Both `.yaml` and `.yml` extensions are accepted.
+- Multi-document YAML files (separated by `---`) are supported.
diff --git a/package.json b/package.json
index 2b3944e3..d934706d 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"@babel/core": ">=7.29.6",
"@opentelemetry/core": ">=2.8.0",
"js-yaml": ">=4.2.0",
+ "gray-matter>js-yaml": "^3.14.1",
"qs": ">=6.15.2",
"uuid": ">=11.1.1",
"yaml": ">=2.8.3"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 06a105b3..42bf41c5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24,6 +24,7 @@ overrides:
'@babel/core': '>=7.29.6'
'@opentelemetry/core': '>=2.8.0'
js-yaml: '>=4.2.0'
+ gray-matter>js-yaml: ^3.14.1
qs: '>=6.15.2'
uuid: '>=11.1.1'
yaml: '>=2.8.3'
@@ -3928,6 +3929,9 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+ argparse@1.0.10:
+ resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
+
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -4842,6 +4846,11 @@ packages:
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
engines: {node: '>=12'}
+ esprima@4.0.1:
+ resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
+ engines: {node: '>=4'}
+ hasBin: true
+
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -5506,6 +5515,10 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
+ js-yaml@3.14.2:
+ resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==}
+ hasBin: true
+
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
hasBin: true
@@ -7264,6 +7277,9 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
+ sprintf-js@1.0.3:
+ resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+
ssh-remote-port-forward@1.0.4:
resolution: {integrity: sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==}
@@ -12234,6 +12250,10 @@ snapshots:
arg@5.0.2: {}
+ argparse@1.0.10:
+ dependencies:
+ sprintf-js: 1.0.3
+
argparse@2.0.1: {}
argv-formatter@1.0.0: {}
@@ -13112,6 +13132,8 @@ snapshots:
escape-string-regexp@5.0.0: {}
+ esprima@4.0.1: {}
+
estree-walker@2.0.2: {}
estree-walker@3.0.3:
@@ -13507,7 +13529,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
- js-yaml: 4.2.0
+ js-yaml: 3.14.2
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@@ -13892,6 +13914,11 @@ snapshots:
js-tokens@9.0.1: {}
+ js-yaml@3.14.2:
+ dependencies:
+ argparse: 1.0.10
+ esprima: 4.0.1
+
js-yaml@4.2.0:
dependencies:
argparse: 2.0.1
@@ -15872,6 +15899,8 @@ snapshots:
split2@4.2.0: {}
+ sprintf-js@1.0.3: {}
+
ssh-remote-port-forward@1.0.4:
dependencies:
'@types/ssh2': 0.5.52