diff --git a/changes/2026-03-22-websocket-texthooker-docs.md b/changes/2026-03-22-websocket-texthooker-docs.md
new file mode 100644
index 0000000..6322acc
--- /dev/null
+++ b/changes/2026-03-22-websocket-texthooker-docs.md
@@ -0,0 +1,5 @@
+type: docs
+area: docs
+
+- Added a new WebSocket / Texthooker API and integration guide covering websocket payloads, custom client patterns, mpv plugin automation, and webhook-style relay examples.
+- Linked the new integration guide from configuration and mining workflow docs for easier discovery.
diff --git a/docs-site/.vitepress/config.ts b/docs-site/.vitepress/config.ts
index f40a3c1..8a1bb2f 100644
--- a/docs-site/.vitepress/config.ts
+++ b/docs-site/.vitepress/config.ts
@@ -95,6 +95,7 @@ export default {
{ text: 'Building & Testing', link: '/development' },
{ text: 'Architecture', link: '/architecture' },
{ text: 'IPC + Runtime Contracts', link: '/ipc-contracts' },
+ { text: 'WebSocket + Texthooker API', link: '/websocket-texthooker-api' },
{ text: 'Changelog', link: '/changelog' },
],
},
diff --git a/docs-site/configuration.md b/docs-site/configuration.md
index be38853..99788bd 100644
--- a/docs-site/configuration.md
+++ b/docs-site/configuration.md
@@ -195,6 +195,8 @@ Defaults warm everything (`true` for all toggles, `lowPowerMode: false`). Settin
The overlay includes a built-in WebSocket server that broadcasts subtitle text to connected clients (such as texthooker-ui) for external processing.
+For endpoint details, payload examples, and client patterns, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
+
By default, the server uses "auto" mode: it starts automatically unless [mpv_websocket](https://github.com/kuroahna/mpv_websocket) is detected at `~/.config/mpv/mpv_websocket`. If you have mpv_websocket installed, the built-in server is skipped to avoid conflicts.
See `config.example.jsonc` for detailed configuration options.
diff --git a/docs-site/mining-workflow.md b/docs-site/mining-workflow.md
index 65e20d2..4d1a500 100644
--- a/docs-site/mining-workflow.md
+++ b/docs-site/mining-workflow.md
@@ -176,6 +176,8 @@ SubMiner runs a local HTTP server at `http://127.0.0.1:5174` (configurable port)
The texthooker page displays the current subtitle and updates as new lines arrive. This is useful if you prefer to do lookups in a browser rather than through the overlay's built-in Yomitan.
+If you want to build your own browser client, websocket consumer, or automation relay, see [WebSocket / Texthooker API & Integration](/websocket-texthooker-api).
+
## Subtitle Sync (Subsync)
If your subtitle file is out of sync with the audio, SubMiner can resynchronize it using [alass](https://github.com/kaegi/alass) or [ffsubsync](https://github.com/smacke/ffsubsync).
diff --git a/docs-site/websocket-texthooker-api.md b/docs-site/websocket-texthooker-api.md
new file mode 100644
index 0000000..01a22f7
--- /dev/null
+++ b/docs-site/websocket-texthooker-api.md
@@ -0,0 +1,357 @@
+# WebSocket / Texthooker API & Integration
+
+SubMiner exposes a small set of local integration surfaces for browser tools, automation helpers, and mpv-driven workflows:
+
+- **Subtitle WebSocket** at `ws://127.0.0.1:6677` by default for plain subtitle pushes.
+- **Annotation WebSocket** at `ws://127.0.0.1:6678` by default for token-aware clients.
+- **Texthooker HTTP UI** at `http://127.0.0.1:5174` by default for browser-based subtitle consumption.
+- **mpv plugin script messages** for in-player automation and extension.
+
+This page documents those integration points and shows how to build custom consumers around them.
+
+## Quick Reference
+
+| Surface | Default | Purpose |
+| --- | --- | --- |
+| `websocket` | `ws://127.0.0.1:6677` | Basic subtitle broadcast stream |
+| `annotationWebsocket` | `ws://127.0.0.1:6678` | Structured stream with token metadata |
+| `texthooker` | `http://127.0.0.1:5174` | Local texthooker UI with injected websocket config |
+| mpv plugin | `script-message subminer-*` | Start/stop/toggle/status automation inside mpv |
+
+## Enable and Configure the Services
+
+SubMiner's integration ports are configured in `config.jsonc`.
+
+```jsonc
+{
+ "websocket": {
+ "enabled": "auto",
+ "port": 6677
+ },
+ "annotationWebsocket": {
+ "enabled": true,
+ "port": 6678
+ },
+ "texthooker": {
+ "launchAtStartup": true,
+ "openBrowser": true
+ }
+}
+```
+
+### How startup behaves
+
+- `websocket.enabled: "auto"` starts the basic subtitle websocket unless SubMiner detects the external `mpv_websocket` plugin.
+- `annotationWebsocket` is independent from `websocket` and stays enabled unless you explicitly disable it.
+- `texthooker.launchAtStartup` starts the local HTTP UI automatically.
+- `texthooker.openBrowser` controls whether SubMiner opens the texthooker page in your browser when it starts.
+
+If you use the [mpv plugin](/mpv-plugin), it can also start a texthooker-only helper process and override the texthooker port in `subminer.conf`.
+
+## Developer API Documentation
+
+### 1. Subtitle WebSocket
+
+Use the basic subtitle websocket when you only need the current subtitle line and a ready-to-render HTML sentence string.
+
+- **Default URL:** `ws://127.0.0.1:6677`
+- **Transport:** local WebSocket server bound to `127.0.0.1`
+- **Direction:** server push only
+- **Client auth:** none
+- **Reconnects:** client-managed
+
+When a client connects, SubMiner immediately sends the latest subtitle payload if one is available. After that, it pushes a new message each time the current subtitle changes.
+
+#### Message shape
+
+```json
+{
+ "version": 1,
+ "text": "無事",
+ "sentence": "無事",
+ "tokens": [
+ {
+ "surface": "無事",
+ "reading": "ぶじ",
+ "headword": "無事",
+ "startPos": 0,
+ "endPos": 2,
+ "partOfSpeech": "other",
+ "isMerged": false,
+ "isKnown": true,
+ "isNPlusOneTarget": false,
+ "isNameMatch": false,
+ "jlptLevel": "N2",
+ "frequencyRank": 745,
+ "className": "word word-known word-jlpt-n2",
+ "frequencyRankLabel": "745",
+ "jlptLevelLabel": "N2"
+ }
+ ]
+}
+```
+
+#### Field reference
+
+| Field | Type | Notes |
+| --- | --- | --- |
+| `version` | number | Current websocket payload version. Today this is `1`. |
+| `text` | string | Raw subtitle text. |
+| `sentence` | string | HTML string with `` wrappers and `data-*` attributes for client rendering. |
+| `tokens` | array | Token metadata; empty when the subtitle is not tokenized yet. |
+
+Each token may include:
+
+| Token field | Type | Notes |
+| --- | --- | --- |
+| `surface` | string | Display text for the token |
+| `reading` | string | Kana reading when available |
+| `headword` | string | Dictionary headword when available |
+| `startPos` / `endPos` | number | Character offsets in the subtitle text |
+| `partOfSpeech` | string | SubMiner token POS label |
+| `isMerged` | boolean | Whether this token represents merged content |
+| `isKnown` | boolean | Marked known by SubMiner's known-word logic |
+| `isNPlusOneTarget` | boolean | True when the token is the sentence's N+1 target |
+| `isNameMatch` | boolean | True for prioritized character-name matches |
+| `frequencyRank` | number | Frequency rank when available |
+| `jlptLevel` | string | JLPT level when available |
+| `className` | string | CSS-ready class list derived from token state |
+| `frequencyRankLabel` | string or `null` | Preformatted rank label for UIs |
+| `jlptLevelLabel` | string or `null` | Preformatted JLPT label for UIs |
+
+### 2. Annotation WebSocket
+
+Use the annotation websocket for custom clients that want the same structured token payload the bundled texthooker UI consumes.
+
+- **Default URL:** `ws://127.0.0.1:6678`
+- **Payload shape:** same JSON contract as the basic subtitle websocket
+- **Primary difference:** this stream is intended to stay on even when the basic websocket auto-disables because `mpv_websocket` is installed
+
+In practice, if you are building a new client, prefer `annotationWebsocket` unless you specifically need compatibility with an existing `websocket` consumer.
+
+### 3. HTML markup conventions
+
+The `sentence` field is pre-rendered HTML generated by SubMiner. Depending on token state, it can include classes such as:
+
+- `word`
+- `word-known`
+- `word-n-plus-one`
+- `word-name-match`
+- `word-jlpt-n1` through `word-jlpt-n5`
+- `word-frequency-single`
+- `word-frequency-band-1` through `word-frequency-band-5`
+
+SubMiner also adds tooltip-friendly data attributes when available:
+
+- `data-reading`
+- `data-headword`
+- `data-frequency-rank`
+- `data-jlpt-level`
+
+If you need a fully custom UI, ignore `sentence` and render from `tokens` instead.
+
+## Texthooker Integration Guide
+
+### When to use the bundled texthooker page
+
+Use texthooker when you want a browser tab that:
+
+- updates live from current subtitles
+- works well with browser-based Yomitan setups
+- inherits SubMiner's coloring preferences and websocket URL automatically
+
+Start it with either:
+
+```bash
+subminer texthooker
+```
+
+or by leaving `texthooker.launchAtStartup` enabled.
+
+### What SubMiner injects into the page
+
+When SubMiner serves the local texthooker UI, it injects bootstrap values into `window.localStorage`, including:
+
+- `bannou-texthooker-websocketUrl`
+- coloring toggles for known/N+1/name/frequency/JLPT styling
+- CSS custom properties for SubMiner's token colors
+
+That means the bundled page already knows which websocket to connect to and which color palette to use.
+
+### Build a custom websocket client
+
+Here is a minimal browser client for the annotation stream:
+
+```html
+
+
+SubMiner client
+Waiting for subtitles...
+
+```
+
+### Build a custom Node client
+
+```js
+import WebSocket from 'ws';
+
+const ws = new WebSocket('ws://127.0.0.1:6678');
+
+ws.on('message', (raw) => {
+ const payload = JSON.parse(String(raw));
+ console.log({
+ text: payload.text,
+ tokens: payload.tokens.length,
+ firstToken: payload.tokens[0]?.surface ?? null,
+ });
+});
+```
+
+### Integration tips
+
+- Bind only to `127.0.0.1`; these services are local-only by design.
+- Handle empty `tokens` arrays gracefully because subtitle text can arrive before tokenization completes.
+- Reconnect on disconnect; SubMiner does not manage client reconnects for you.
+- Prefer `payload.text` for logging/automation and `payload.sentence` or `payload.tokens` for UI rendering.
+
+## Plugin Development
+
+SubMiner does **not** currently expose a general-purpose third-party plugin SDK inside the app itself. Today, the supported extension surfaces are:
+
+1. the local websocket streams
+2. the local texthooker UI
+3. the mpv Lua plugin's script-message API
+4. the launcher CLI
+
+### mpv script messages
+
+The mpv plugin accepts these script messages:
+
+```text
+script-message subminer-start
+script-message subminer-stop
+script-message subminer-toggle
+script-message subminer-menu
+script-message subminer-options
+script-message subminer-restart
+script-message subminer-status
+script-message subminer-autoplay-ready
+script-message subminer-aniskip-refresh
+script-message subminer-skip-intro
+```
+
+The start command also accepts inline overrides:
+
+```text
+script-message subminer-start backend=hyprland socket=/custom/path texthooker=no log-level=debug
+```
+
+### Practical extension patterns
+
+#### Add another mpv script that coordinates with SubMiner
+
+Examples:
+
+- send `subminer-start` after your own media-selection script chooses a file
+- send `subminer-status` before running follow-up automation
+- send `subminer-aniskip-refresh` after you update title/episode metadata
+
+#### Build a launcher wrapper
+
+Examples:
+
+- open a media picker, then call `subminer /path/to/file.mkv`
+- launch browser-only subtitle tooling with `subminer texthooker`
+- disable the helper UI for a session with `subminer --no-texthooker video.mkv`
+
+#### Build an overlay-adjacent client
+
+Examples:
+
+- browser widget showing current subtitle + token breakdown
+- local vocabulary capture helper that writes interesting lines to a file
+- bridge service that forwards websocket events into your own workflow engine
+
+## Webhook Examples
+
+SubMiner does **not** currently send outbound webhooks by itself. The supported pattern is to consume the websocket locally and relay events into another system.
+
+That still makes webhook-style automation straightforward.
+
+### Example: forward subtitle lines to a local webhook receiver
+
+```js
+import WebSocket from 'ws';
+
+const ws = new WebSocket('ws://127.0.0.1:6678');
+
+ws.on('message', async (raw) => {
+ const payload = JSON.parse(String(raw));
+
+ await fetch('http://127.0.0.1:5678/subminer/subtitle', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({
+ text: payload.text,
+ tokens: payload.tokens,
+ receivedAt: new Date().toISOString(),
+ }),
+ });
+});
+```
+
+### Automation ideas
+
+- **n8n / Make / Zapier relay:** send each subtitle line into an automation workflow for logging, translation, or summarization.
+- **Discord / Slack notifier:** post only lines that contain unknown words or N+1 targets.
+- **Obsidian / Markdown capture:** append subtitle lines plus token metadata to a daily immersion note.
+- **Local LLM pipeline:** trigger a glossary, translation, or sentence-mining workflow whenever a new line arrives.
+
+### Filtering example: only forward N+1 lines
+
+```js
+import WebSocket from 'ws';
+
+const ws = new WebSocket('ws://127.0.0.1:6678');
+
+ws.on('message', async (raw) => {
+ const payload = JSON.parse(String(raw));
+ const hasNPlusOne = payload.tokens.some((token) => token.isNPlusOneTarget);
+
+ if (!hasNPlusOne) return;
+
+ await fetch('http://127.0.0.1:5678/subminer/n-plus-one', {
+ method: 'POST',
+ headers: { 'content-type': 'application/json' },
+ body: JSON.stringify({ text: payload.text, tokens: payload.tokens }),
+ });
+});
+```
+
+## Recommended Integration Combinations
+
+- **Browser Yomitan client:** `texthooker` + `annotationWebsocket`
+- **Custom dashboard:** `annotationWebsocket` only
+- **Lightweight subtitle mirror:** `websocket` only
+- **mpv-side automation:** mpv plugin script messages + optional websocket relay
+- **Webhook-style workflows:** `annotationWebsocket` + your own local relay service
+
+## Related Pages
+
+- [Configuration](/configuration#websocket-server)
+- [Mining Workflow — Texthooker](/mining-workflow#texthooker)
+- [MPV Plugin](/mpv-plugin)
+- [Launcher Script](/launcher-script)
+- [Anki Integration](/anki-integration#proxy-mode-setup-yomitan--texthooker)