Local API

WhisperTyping includes a Local API that lets you integrate voice typing into your own projects. We believe there are a lot of possibilities here: connecting WhisperTyping to Stream Decks, home automation, AI agents, accessibility tools, and things we haven't even thought of yet. This is a work in progress, and we're always expanding it. If you build something cool with it, we'd love to hear about your project.

The API has two parts. The Command API lets you send commands to WhisperTyping via HTTP requests: control recording, change settings, manage your vocabulary and text replacements, and more. The Transcription Hook works the other way around: WhisperTyping sends each transcription to a service you specify, so you can intercept voice commands, process text, or trigger automations before anything is typed.

The Command API listens on port 39849 on localhost. The Transcription Hook sends requests to whatever URL you configure, which can be a local service or a remote server anywhere on the internet.

Base URL: http://localhost:39849

Table of Contents

Getting Started

The Local API is enabled by default. You can test it right away by opening a terminal and running:

# See all available endpoints
curl http://localhost:39849/

# Check app status
curl http://localhost:39849/api/status

# Start recording
curl -X POST http://localhost:39849/api/recording/start

# Stop recording (transcribes the audio)
curl -X POST http://localhost:39849/api/recording/stop

The base URL (http://localhost:39849/) returns the app version, current state, a link to this documentation, and a list of all available endpoints. This is a good starting point to verify the API is running and to discover what's available.

You can also call the API from JavaScript in the browser using fetch(). Browser requests are blocked by default for security. To allow browser access, go to Settings → Advanced and add your origin to "Allowed browser origins" (e.g. http://localhost:3000), or use * to allow all origins.

All endpoints return JSON:

{ "success": true }

If something goes wrong:

{ "success": false, "error": "Not currently recording" }

Settings

You can configure the Local API in Settings → Advanced:

Enable Local API Master switch to turn the API server on or off. Enabled by default.
Allowed browser origins Controls which websites can access the API. Leave empty to block all browser requests. Enter * to allow all, or list specific origins separated by commas (e.g. http://localhost:3000). Scripts and desktop applications are always allowed.
Transcription hook URL URL to send each transcription to before it is typed. Leave empty to disable. Can also be set via the API.

Command API

Status

MethodEndpointDescription
GET/api/statusGet current app status

Response:

{
  "success": true,
  "recording": false,
  "state": "idle",
  "model": "Whisper Ultra",
  "language": "en",
  "toolbar": "compact",
  "version": "4.4.5"
}

The state field is one of: idle, recording, processing.

Recording

MethodEndpointDescription
GET/POST/api/recording/startStart recording (dictation)
GET/POST/api/recording/stopStop recording and transcribe
GET/POST/api/recording/cancelCancel current recording without transcribing
GET/POST/api/recording/toggleToggle: starts if idle, stops if recording

Recording started via the API works the same as recording started via a hotkey. Your AI mode, language, text replacements, and all other settings apply normally.

Request:

curl -X POST http://localhost:39849/api/recording/start

Response:

{ "success": true }

Error response (already recording):

{ "success": false, "error": "Cannot start recording - currently recording" }

Model

MethodEndpointDescription
GET/api/modelGet current model and available options
POST/api/modelChange transcription model

GET response:

{
  "success": true,
  "model": "Whisper Ultra",
  "options": ["Whisper Large", "Whisper Ultra", "Ultra Medical (English)"]
}

The available options depend on your subscription tier. To change the model:

POST request:

curl -X POST http://localhost:39849/api/model \
  -H "Content-Type: application/json" \
  -d '{ "model": "Whisper Large" }'

POST response:

{ "success": true, "model": "Whisper Large" }

Language

MethodEndpointDescription
GET/api/languageGet current language and available options
POST/api/languageChange language

GET response:

{
  "success": true,
  "language": "en",
  "model": "Whisper Ultra",
  "options": [
    { "code": "", "name": "Auto-Detect" },
    { "code": "en", "name": "English" },
    { "code": "nl", "name": "Dutch" }
  ]
}

Available languages depend on the current model. Most models support 51 languages plus auto-detect. Medical models only support English variants. Use an empty string "" for auto-detect.

POST request:

curl -X POST http://localhost:39849/api/language \
  -H "Content-Type: application/json" \
  -d '{ "language": "nl" }'

POST response:

{ "success": true, "language": "nl", "model": "Whisper Ultra" }

Toolbar

MethodEndpointDescription
GET/api/toolbarGet current toolbar mode and options
POST/api/toolbarChange toolbar mode

Three modes are available:

POST request:

curl -X POST http://localhost:39849/api/toolbar \
  -H "Content-Type: application/json" \
  -d '{ "mode": "compact" }'

POST response:

{ "success": true, "toolbar": "compact" }

Vocabulary

The vocabulary is a list of words and phrases that helps the speech recognition model transcribe them correctly. This is especially useful for proper names, technical terms, abbreviations, and domain-specific jargon.

A vocabulary entry can be a single word or a multi-word phrase. For example, Dr. Smith, Microsoft Azure, or amoxicillin are all valid entries. The vocabulary is stored as a comma-separated string internally.

MethodEndpointDescription
GET/api/vocabularyGet the current vocabulary
POST/api/vocabularySet the entire vocabulary (replaces all)
POST/api/vocabulary/addAdd words to the vocabulary (skips duplicates)
POST/api/vocabulary/removeRemove words from the vocabulary

Get Vocabulary

curl http://localhost:39849/api/vocabulary

Response:

{
  "success": true,
  "vocabulary": "Dr. Smith, amoxicillin, WhisperTyping",
  "words": ["Dr. Smith", "amoxicillin", "WhisperTyping"],
  "characterCount": 39,
  "characterLimit": 500
}

Set Vocabulary

Replaces the entire vocabulary with a new value.

curl -X POST http://localhost:39849/api/vocabulary \
  -H "Content-Type: application/json" \
  -d '{ "vocabulary": "Dr. Smith, amoxicillin, WhisperTyping" }'

Response: Returns the same format as GET (the new vocabulary state).

Add Words

Adds new entries to the existing vocabulary. Duplicates are skipped (case-insensitive comparison). Each entry in the words array can be a single word or a multi-word phrase.

curl -X POST http://localhost:39849/api/vocabulary/add \
  -H "Content-Type: application/json" \
  -d '{ "words": ["John Williams", "penicillin", "WhisperTyping"] }'

Response:

{
  "success": true,
  "added": ["John Williams", "penicillin"],
  "skipped": ["WhisperTyping"],
  "vocabulary": "Dr. Smith, amoxicillin, WhisperTyping, John Williams, penicillin",
  "characterCount": 62,
  "characterLimit": 500
}

Remove Words

Removes entries from the vocabulary. Matching is case-insensitive.

curl -X POST http://localhost:39849/api/vocabulary/remove \
  -H "Content-Type: application/json" \
  -d '{ "words": ["amoxicillin", "nonexistent"] }'

Response:

{
  "success": true,
  "removed": ["amoxicillin"],
  "notFound": ["nonexistent"],
  "vocabulary": "Dr. Smith, WhisperTyping, John Williams, penicillin",
  "characterCount": 50,
  "characterLimit": 500
}

Screen OCR

When Screen OCR is enabled, WhisperTyping reads text from the active window before each recording and uses it to improve transcription accuracy. This helps with names, numbers, and context-specific terms visible on screen.

MethodEndpointDescription
GET/api/ocrCheck if Screen OCR is enabled
POST/api/ocrEnable or disable Screen OCR

Get OCR Status

curl http://localhost:39849/api/ocr

Response:

{ "success": true, "enabled": false }

Enable/Disable OCR

curl -X POST http://localhost:39849/api/ocr \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'

Response:

{ "success": true, "enabled": true }

Text Replacements

Text replacements automatically transform spoken text after transcription. For example, you can replace "my email" with your actual email address, or fix common transcription errors. Replacements are applied in order, so the position matters.

Each replacement has a unique id for stable referencing. The id is auto-generated and never changes, even when replacements are reordered.

MethodEndpointDescription
GET/api/replacementsGet all text replacements
POST/api/replacementsSet all text replacements (replaces entire list)
POST/api/replacements/addAdd a new replacement
GET/api/replacements/{id}Get a single replacement by id
POST/api/replacements/{id}/updateUpdate a replacement (partial update)
POST/api/replacements/{id}/deleteDelete a replacement
POST/api/replacements/{id}/moveMove a replacement to a new position

Replacement Object

Each replacement has the following fields:

FieldTypeDefaultDescription
idstring(auto)Unique identifier (auto-generated, read-only).
positioninteger(auto)Position in the list (0-based, read-only).
matchstring[][]One or more patterns to match. Multiple entries act as aliases (any of them will trigger the replacement).
replacestring""The replacement text.
scopestring"Anywhere"Where to match: Anywhere, WholeWord, StartOnly, or EndOnly.
preserveCasebooleanfalseIf true, the replacement preserves the case of the matched text (ALL CAPS, lowercase, Title Case).
includeVariantsbooleanfalseIf true, also matches variations with separators (e.g. "hash-tag" and "hash tag" both match "hashtag").
swallowPunctuationsbooleanfalseIf true, punctuation immediately following the matched text is removed.
isRegexbooleanfalseIf true, the match pattern is treated as a regular expression instead of literal text.
enabledbooleantrueIf false, the replacement is skipped during processing.

Get All Replacements

curl http://localhost:39849/api/replacements

Response:

{
  "success": true,
  "replacements": [
    {
      "id": "a1b2c3d4e5f6789012345678abcdef01",
      "position": 0,
      "match": ["my email"],
      "replace": "[email protected]",
      "scope": "WholeWord",
      "preserveCase": false,
      "includeVariants": false,
      "swallowPunctuations": false,
      "isRegex": false,
      "enabled": true
    },
    {
      "id": "fedcba9876543210fedcba9876543210",
      "position": 1,
      "match": ["hashtag", "hash tag"],
      "replace": "#",
      "scope": "Anywhere",
      "preserveCase": false,
      "includeVariants": true,
      "swallowPunctuations": false,
      "isRegex": false,
      "enabled": true
    }
  ],
  "count": 2
}

Set All Replacements

Replaces the entire replacement list. Any replacement without an id gets one auto-assigned. Useful for bulk configuration.

curl -X POST http://localhost:39849/api/replacements \
  -H "Content-Type: application/json" \
  -d '{
    "replacements": [
      {
        "match": ["my email"],
        "replace": "[email protected]",
        "scope": "WholeWord",
        "enabled": true
      },
      {
        "match": ["my phone"],
        "replace": "+1 555-0123",
        "scope": "WholeWord",
        "enabled": true
      }
    ]
  }'

Response: Returns the same format as GET (the full list with auto-assigned ids).

Add a Replacement

Adds a single replacement to the end of the list. An id is auto-generated. Only match is required; all other fields use their defaults if omitted.

curl -X POST http://localhost:39849/api/replacements/add \
  -H "Content-Type: application/json" \
  -d '{
    "match": ["my address"],
    "replace": "123 Main Street, Springfield",
    "scope": "WholeWord"
  }'

Response:

{
  "success": true,
  "replacement": {
    "id": "01234567890abcdef01234567890abcd",
    "position": 2,
    "match": ["my address"],
    "replace": "123 Main Street, Springfield",
    "scope": "WholeWord",
    "preserveCase": false,
    "includeVariants": false,
    "swallowPunctuations": false,
    "isRegex": false,
    "enabled": true
  }
}

Get a Single Replacement

curl http://localhost:39849/api/replacements/a1b2c3d4e5f6789012345678abcdef01

Response:

{
  "success": true,
  "replacement": {
    "id": "a1b2c3d4e5f6789012345678abcdef01",
    "position": 0,
    "match": ["my email"],
    "replace": "[email protected]",
    "scope": "WholeWord",
    "preserveCase": false,
    "includeVariants": false,
    "swallowPunctuations": false,
    "isRegex": false,
    "enabled": true
  }
}

Update a Replacement

Updates specific fields of an existing replacement. Only include the fields you want to change. Fields not included keep their current value.

curl -X POST http://localhost:39849/api/replacements/a1b2c3d4e5f6789012345678abcdef01/update \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

Response:

{
  "success": true,
  "replacement": {
    "id": "a1b2c3d4e5f6789012345678abcdef01",
    "position": 0,
    "match": ["my email"],
    "replace": "[email protected]",
    "scope": "WholeWord",
    "preserveCase": false,
    "includeVariants": false,
    "swallowPunctuations": false,
    "isRegex": false,
    "enabled": false
  }
}

You can update multiple fields at once:

curl -X POST http://localhost:39849/api/replacements/a1b2c3d4e5f6789012345678abcdef01/update \
  -H "Content-Type: application/json" \
  -d '{ "replace": "[email protected]", "match": ["my email", "email address"] }'

Delete a Replacement

curl -X POST http://localhost:39849/api/replacements/a1b2c3d4e5f6789012345678abcdef01/delete

Response:

{ "success": true }

Error (not found):

{ "success": false, "error": "Replacement not found: a1b2c3d4e5f6789012345678abcdef01" }

Move a Replacement

Moves a replacement to a new position in the list. Positions are 0-based. The position is clamped to the valid range.

curl -X POST http://localhost:39849/api/replacements/fedcba9876543210fedcba9876543210/move \
  -H "Content-Type: application/json" \
  -d '{ "position": 0 }'

Response:

{
  "success": true,
  "replacement": {
    "id": "fedcba9876543210fedcba9876543210",
    "position": 0,
    "match": ["hashtag", "hash tag"],
    "replace": "#",
    "scope": "Anywhere",
    "preserveCase": false,
    "includeVariants": true,
    "swallowPunctuations": false,
    "isRegex": false,
    "enabled": true
  }
}

Transcription Hook Configuration

MethodEndpointDescription
GET/api/hookGet current hook URL
POST/api/hookSet or clear the hook URL

Get Hook Config

curl http://localhost:39849/api/hook

Response:

{ "success": true, "url": "http://localhost:8585/hook", "enabled": true }

Set Hook URL

curl -X POST http://localhost:39849/api/hook \
  -H "Content-Type: application/json" \
  -d '{ "url": "http://localhost:8585/hook" }'

Response:

{ "success": true, "url": "http://localhost:8585/hook", "enabled": true }

Set an empty string to disable the hook:

curl -X POST http://localhost:39849/api/hook \
  -H "Content-Type: application/json" \
  -d '{ "url": "" }'

Output Window

MethodEndpointDescription
GET/api/window/outputGet available icon names
POST/api/window/outputDisplay content in the output window

Display custom content in WhisperTyping's output window. This brings the window to the foreground. All fields are optional, but at least title or content must be provided.

POST request:

curl -X POST http://localhost:39849/api/window/output \
  -H "Content-Type: application/json" \
  -d '{ "title": "Home", "icon": "home", "content": "Lights turned off." }'

Response:

{ "success": true }

The icon field is optional. If omitted, a default icon is used. Available icons: microphone, edit, comment, terminal, reply, question, check, info, warning, error, home, lightbulb, gear, search, bolt, star, heart, flag, bell, clipboard, keyboard, play, globe, code, link.

Settings Pages

Open specific settings pages in the WhisperTyping window.

MethodEndpointDescription
POST/api/window/settings/generalOpen Settings → General
POST/api/window/settings/microphoneOpen Settings → Microphone
POST/api/window/settings/hotkeysOpen Settings → Hotkeys
POST/api/window/settings/vocabularyOpen Settings → Vocabulary
POST/api/window/settings/replacementsOpen Settings → Replacements
POST/api/window/settings/aimodesOpen Settings → AI Modes
POST/api/window/settings/appearanceOpen Settings → Appearance
POST/api/window/settings/historyOpen Settings → History
POST/api/window/settings/advancedOpen Settings → Advanced
POST/api/window/settings/mobileOpen Settings → Mobile

Request:

curl -X POST http://localhost:39849/api/window/settings/vocabulary

Response:

{ "success": true, "page": "vocabulary" }

Exit

MethodEndpointDescription
POST/api/exitShut down WhisperTyping
curl -X POST http://localhost:39849/api/exit

Response:

{ "success": true }

Transcription Hook

The transcription hook lets an external service receive every transcription before it is typed. Your service decides what happens: let the text through normally, or intercept it and optionally display output in WhisperTyping's window.

This enables use cases like home automation voice commands, AI agents, custom command systems, logging, and more.

How It Works

The hook fires after text replacements and spoken punctuation are applied, but before AI mode processing:

Pipeline: Speech → Transcribe → Replacements & Punctuation → Hook → AI Mode → Virtual Keyboard

Configure the hook URL in Settings → Advanced under "Transcription hook URL", or via the API (POST /api/hook).

What Your Service Receives

WhisperTyping sends a POST request with JSON for each transcription:

POST http://your-service-url
Content-Type: application/json

{
  "text": "Turn off all the lights in my office.",
  "verbatim": "turn off all the lights in my office",
  "timestamp": "2026-02-21T12:00:00.000Z",
  "language": "en"
}

How to Respond

Your service responds with a JSON object. All fields are optional. An empty response {} lets the text through normally.

FieldTypeDefaultDescription
interceptbooleanfalseIf true, the text is blocked. Nothing is typed and no AI mode is applied.
skip_aibooleanfalseIf true, AI mode processing is skipped. The text goes directly to the virtual keyboard. Has no effect when intercept is true.
textstringnullOverride the transcription text. The overridden text continues through the rest of the pipeline (AI mode, virtual keyboard). If null or absent, the original text is used.
titlestringnullTitle text shown in the WhisperTyping output window header.
iconstringnullIcon shown in the output window header. See available icons. If omitted, a default icon is used.
contentstringnullBody text shown in the output window panel.

If title or content is provided, the WhisperTyping output window is brought to the foreground and displays the content. This works with any combination of flags.

Examples

Let it through (default):

{}

WhisperTyping continues normally: applies AI mode if active, then types the text.

Override the text:

{ "text": "The corrected transcription text." }

Replaces the transcription text with a new value. The overridden text continues through the pipeline normally (AI mode, virtual keyboard). Useful for fixing transcription errors, translating, or preprocessing text before it's typed.

Intercept and show output:

{
  "intercept": true,
  "title": "Home Automation",
  "icon": "home",
  "content": "Lights turned off in the office."
}

The text is blocked and a message is displayed in the output window.

Skip AI mode:

{ "skip_ai": true }

The text is typed directly, bypassing AI mode. Useful when your service has already processed the text.

Timeout

The default timeout is 30 seconds. If your service doesn't respond in time, WhisperTyping continues normally and types the text. While waiting for a response, the app shows "Handling" status.

Security

The Local API is designed to be safe by default:

Allowing Browser Access

If you're building a web-based dashboard or control panel that runs in your browser, you'll need to add its origin to the allowed list in Settings → Advanced. For example, if your dashboard runs at http://localhost:3000, add that as an allowed origin.

Use Cases

Get in Touch

We'd love to hear from you. Whether you're integrating WhisperTyping into your software, building a hobby project, or have an idea for a use case we haven't thought of, let us know!

Tell us about your project

What are you building with the Local API? What endpoints would be most useful to you? Your feedback helps us prioritize what to build next.