Note
Local only: The server is disabled by default and binds only to 127.0.0.1. Do not proxy or expose it to a public network.
Setup and authentication
Enable API Server in Settings > Advanced. The default port is 8978; TypeWhisper writes the active port and a generated token to ~/Library/Application Support/TypeWhisper/api-discovery.json while the server is running.
From TypeWhisper 1.8, the API server requires the token by default: send the discovery token as a Bearer token on every endpoint except GET /v1/status. If the API server was turned on without a token when you updated to 1.8, it keeps working without one until you turn on Require API Token; Settings shows a warning until then. Settings > Advanced > Copy API Token copies the token to the clipboard. Pass the token in a header file, as shown below, rather than on the command line: macOS shows the command lines of running programs to every user of the Mac, and mktemp creates a file only you can read. The example runs in a subshell whose trap deletes the file afterwards. The other examples leave out the token header for brevity: create the header file the same way and add -H @"$AUTH_HEADER" to every request except GET /v1/status.
DISCOVERY="$HOME/Library/Application Support/TypeWhisper/api-discovery.json"
export TYPEWHISPER_API_PORT="$(jq -r '.port' "$DISCOVERY")"
(
AUTH_HEADER="$(mktemp)" || exit
trap 'rm -f "$AUTH_HEADER"' EXIT
jq -r '"Authorization: Bearer " + .token' "$DISCOVERY" > "$AUTH_HEADER"
curl "http://127.0.0.1:$TYPEWHISPER_API_PORT/v1/models" -H @"$AUTH_HEADER"
)GET /v1/status remains public for readiness checks. Clients may alternatively send the same token in the X-TypeWhisper-API-Token header.
The discovery file is created with owner-only permissions (0600) and removed when the server stops. The TypeWhisper CLI discovers both its port and token automatically; --port, --api-token, and TYPEWHISPER_API_TOKEN provide explicit overrides.
From TypeWhisper 1.8, and independent of the token, the server answers 403 to requests that browsers send on behalf of other websites: requests with a Host header other than 127.0.0.1, localhost, or [::1], with an Origin from a website not served from this Mac (including null), or with Sec-Fetch-Site: cross-site unless the Origin is a page served from this Mac. Requests without these headers, such as those from curl or scripts, are not affected.
Endpoint reference
The tables below list the documented routes of the macOS implementation by purpose, including the two legacy /v1/profiles aliases and the settings backup routes.
Status, models, and transcription
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /v1/status | Report readiness, selected engine/model, API version, and capability flags. |
| GET | /v1/models | List every model exposed by loaded transcription engines and its current state. |
| POST | /v1/models/load | Download a model if needed, load it, and select it for transcription. |
| POST | /v1/models/unload | Unload the active model of an engine and keep its downloaded files. |
| DELETE | /v1/models | Delete the downloaded files of a model. |
| POST | /v1/transcribe | Transcribe multipart or raw audio, with optional language, engine, model, translation, prompt, and correction controls. |
| POST | /v1/transcribe/local-file | Transcribe an absolute file path already accessible to the running Mac app; used by the CLI to avoid uploading local files. |
History, Dictionary, and settings
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /v1/history | Search and paginate transcription history. |
| DELETE | /v1/history | Delete one history entry by UUID. |
| PUT | /v1/history/speakers/name | Set the name of a speaker in a History speaker transcript. Available from TypeWhisper 1.8. |
| POST | /v1/history/speakers/enroll | Save a named speaker's voice in a voice profile so TypeWhisper recognizes it. Needs Premium. Available from TypeWhisper 1.8. |
| POST | /v1/history/speakers/confirm | Confirm the name a voice profile suggested for a speaker. Needs Premium. Available from TypeWhisper 1.8. |
| GET | /v1/dictionary/terms | List enabled recognition terms and optional CTC similarity thresholds. |
| PUT | /v1/dictionary/terms | Merge or replace recognition terms. |
| DELETE | /v1/dictionary/terms | Delete one recognition term. |
| GET | /v1/dictionary/corrections | List post-transcription Dictionary Corrections. |
| PUT | /v1/dictionary/corrections | Add or update one Dictionary Correction. |
| DELETE | /v1/dictionary/corrections | Delete one Dictionary Correction by its original text. |
| GET | /v1/settings/export | Export the complete Settings Backup JSON document. |
| POST | /v1/settings/import | Import a Settings Backup document and return a machine-readable merge/skip summary. |
| GET | /v1/settings/audio | Read the microphone priority list, the connected inputs, and the recording audio options. Available from TypeWhisper 1.8. |
| PATCH | /v1/settings/audio | Change the microphone priority list and the recording audio options. Available from TypeWhisper 1.8. |
Workflows, dictation, and Recorder
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /v1/rules | List workflow-backed automation rules. |
| PUT | /v1/rules/toggle | Toggle one workflow-backed rule by UUID. |
| GET | /v1/profiles | Legacy alias for GET /v1/rules. |
| PUT | /v1/profiles/toggle | Legacy alias for PUT /v1/rules/toggle. |
| POST | /v1/dictation/start | Start system-wide dictation, optionally forcing an enabled workflow. |
| POST | /v1/dictation/stop | Stop the active API dictation session. |
| GET | /v1/dictation/status | Report the live dictation state, model, and active workflow. |
| GET | /v1/dictation/transcription | Poll a dictation session for completion, transcription, or failure. |
| PUT | /v1/browser/context | Report the browser's active tab from an extension, so website workflows match. Available from TypeWhisper 1.8. |
| POST | /v1/recorder/start | Start Recorder with optional microphone and system-audio overrides. |
| POST | /v1/recorder/stop | Stop and begin finalizing the active API Recorder session. |
| GET | /v1/recorder/status | Report whether Recorder is currently recording. |
| GET | /v1/recorder/session | Poll a Recorder session for status, text, output file, or failure. |
| GET | /v1/recorder/recordings | List the latest saved transcript of each recording, filtered by the time it was saved. Available from TypeWhisper 1.8. |
Text processing
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /v1/llm/models | List the language models of this Mac and whether they are ready. Available from TypeWhisper 1.8. |
| POST | /v1/llm/process | Process text with a language model and your own system prompt. Available from TypeWhisper 1.8. |
| GET | /v1/workflows | List the enabled workflows that can process text without a text field. Available from TypeWhisper 1.8. |
| POST | /v1/workflows/run | Run a workflow on text and return the result without inserting it. Available from TypeWhisper 1.8. |
Check status
curl http://localhost:8978/v1/status{
"status": "ready",
"engine": "whisper",
"model": "openai_whisper-large-v3_turbo",
"api_version": "1.2",
"supports_workflow_dictation": true,
"supports_streaming": true,
"supports_translation": true,
"supports_browser_context": true,
"capabilities": ["transcribe", "llm", "workflows"],
"remote_writing_allowed": false
}status is ready when the selected model can transcribe and no_model otherwise. Capability fields describe the currently selected engine; nullable engine and model values are omitted when none is selected.
From TypeWhisper 1.8, the response also contains supports_browser_context, which is always true and tells a browser extension that it can report the active tab, capabilities with transcribe, llm, and workflows, and remote_writing_allowed, which says whether paired iPhones and iPads may use the text processing routes.
Transcribe audio
curl -X POST http://localhost:8978/v1/transcribe \
-F "file=@recording.wav" \
-F "language_hint=de" \
-F "language_hint=en" \
-F "response_format=verbose_json"{
"text": "Hello, world!",
"language": "en",
"duration": 2.5,
"processing_time": 0.8,
"engine": "whisper",
"model": "openai_whisper-large-v3_turbo",
"segments": [
{ "start": 0, "end": 2.5, "text": "Hello, world!" }
]
}Note
Upload limit: Multipart and raw-body uploads to /v1/transcribe are limited to 256 MiB. Larger requests return 413 Payload Too Large. The macOS CLI uses the local-file route for file paths, while stdin remains subject to this limit.
Multipart parameters
language– An exact language code such as en or de. Omit it for automatic detection; do not combine it with language_hint.language_hint– A repeatable, ordered shortlist for restricted automatic detection. Hint-aware engines receive the full list; other engines use the first hint.task– transcribe (default) or translate. The translate task translates into English and requires WhisperKit.target_language– A target language code for a separate Apple Translate step. This requires macOS 15 or later.response_format– json (default) or verbose_json. Verbose output adds timestamped segments and optional speaker metadata when the engine provides it.prompt– A request-specific transcription prompt. TypeWhisper combines it with enabled Dictionary terms.engine/model– Per-request engine/model overrides. A model alone is inferred only when exactly one engine offers that ID.normalize_numbers– A Boolean override for spoken-number normalization.apply_corrections– A Boolean that defaults to true. Set it to false to return raw engine output without post-transcription Dictionary Corrections. From TypeWhisper 1.8, the corrections also apply to the segments of averbose_jsonresponse.detect_speakers– Available from TypeWhisper 1.8. A Boolean, defaultfalse.trueadds the speaker to each segment of averbose_jsonresponse and needs Premium; see Speaker detection below.speaker_count– Available from TypeWhisper 1.8. The number of speakers, from 2 to 8, if you know it. Without it, TypeWhisper detects the number. Only used withdetect_speakers.?await_download=1– Wait for an unconfigured local engine to restore or download its model instead of returning 409 immediately.
Raw-body uploads use the Content-Type to infer the audio format and the corresponding X-Language, X-Language-Hints, X-Task, X-Target-Language, X-Response-Format, X-Prompt, X-Engine, X-Model, X-Normalize-Numbers, and X-Apply-Corrections headers for options. From TypeWhisper 1.8, X-Detect-Speakers and X-Speaker-Count set the speaker options.
POST /v1/transcribe/local-file accepts a JSON body with an absolute path and the same snake_case options. It is only useful for files readable by the local TypeWhisper process and is not a remote file API.
Speaker detection
Available from TypeWhisper 1.8.
With detect_speakers=true, TypeWhisper works out who spoke when and adds speaker (Speaker 1, Speaker 2, …) and, when available, speaker_confidence from 0 to 1 to each segment of a verbose_json response. The text stays the same. Speaker detection is part of Premium and runs on this Mac with the Speaker Detection plugin. If the engine already assigned the segments to speakers, TypeWhisper keeps its labels unless you pass speaker_count. POST /v1/transcribe/local-file accepts detect_speakers and speaker_count in its JSON body. Use it with response_format=verbose_json; with other formats speaker detection still runs, but the response does not show it.
curl -X POST http://localhost:8978/v1/transcribe \
-F "file=@interview.m4a" \
-F "response_format=verbose_json" \
-F "detect_speakers=true" \
-F "speaker_count=2"{
"text": "How did the launch go? Better than we expected.",
"language": "en",
"duration": 5.8,
"processing_time": 2.1,
"engine": "parakeet",
"model": "parakeet-tdt-0.6b-v3",
"segments": [
{
"start": 0,
"end": 1.9,
"text": "How did the launch go?",
"speaker": "Speaker 1",
"speaker_confidence": 1
},
{
"start": 2.3,
"end": 5.8,
"text": "Better than we expected.",
"speaker": "Speaker 2",
"speaker_confidence": 1
}
]
}Without Premium, the request returns 403. It returns 503 when speaker detection is not available, for example because the Speaker Detection plugin is turned off, and 422 when the engine returned no timestamps. An invalid detect_speakers value or a speaker_count outside 2 to 8 returns 400. If the speaker detection models are missing, the first request downloads them; if managed settings do not allow model downloads, it returns 403.
List models
curl http://localhost:8978/v1/models{
"models": [
{
"id": "openai_whisper-large-v3_turbo",
"engine": "whisper",
"engine_name": "WhisperKit",
"name": "Large v3 Turbo",
"size_description": "~800 MB",
"language_count": 99,
"status": "ready",
"selected": true,
"downloaded": true,
"loaded": true
}
]
}status is ready or not_configured. selected identifies the current GUI selection; downloaded and loaded are included when the engine can report those states.
From TypeWhisper 1.8, engine_name is the display name of the engine, such as WhisperKit for whisper.
Load, unload, and delete models
Pass the engine and id values from GET /v1/models as engine and model. POST /v1/models/load downloads the model if needed, loads it, and selects it for transcription. The request returns once the model is ready or loading has failed; a download can keep it open for up to ten minutes.
curl -X POST http://localhost:8978/v1/models/load \
-H "Content-Type: application/json" \
-d '{"engine":"parakeet","model":"parakeet-tdt-0.6b-v3"}'
curl -X POST http://localhost:8978/v1/models/unload \
-H "Content-Type: application/json" \
-d '{"engine":"parakeet"}'
curl -X DELETE "http://localhost:8978/v1/models?engine=whisper&model=openai_whisper-large-v3_turbo"{
"engine": "parakeet",
"model": "parakeet-tdt-0.6b-v3",
"status": "ready"
}Each route returns engine, model, and status, which is ready, unloaded, or deleted. POST /v1/models/unload takes only engine: it unloads the engine's model and keeps the downloaded files. Its model names the loaded model, or the engine's selected model if none is loaded, and is left out when there is neither.
DELETE /v1/models takes engine and model as query parameters and removes the downloaded model files. This works only for engines whose plugin manages its own model downloads, such as WhisperKit (whisper). Other engines, such as Parakeet or cloud engines, return 409.
Invalid JSON or a missing engine returns 400, and so does a missing model for load and delete. An unknown engine or a model the engine does not offer returns 404, and so does deleting a model that is not downloaded. The routes return 409 when the engine cannot perform the action, is busy, or loading the model fails or takes too long. Cloud engines, for example, have no local model to unload.
From TypeWhisper 1.8, when managed settings enforce a transcription engine and model, POST /v1/models/load returns 403 for any other engine or model, and also when the enforced pair cannot be used on this Mac.
History
curl "http://localhost:8978/v1/history?q=meeting&limit=10&offset=0"
curl -X DELETE "http://localhost:8978/v1/history?id=<uuid>"q is optional. limit defaults to 50 and is capped at 200; offset defaults to 0. Results include final and raw text, ISO 8601 timestamp, app context, duration, language, engine, model, and word count.
Speakers and Recorder recordings
Available from TypeWhisper 1.8.
Entries with speaker detection carry the following fields, and include=speaker_segments adds their segments. Speaker detection is part of Premium. Fields that do not apply to an entry are left out.
curl "http://localhost:8978/v1/history?limit=10&include=speaker_segments"{
"entries": [
{
"id": "0B7A3C1E-2F4D-4E8A-9C61-3D5E7F9A1B2C",
"text": "Let's start with the roadmap. Sounds good to me.",
"raw_text": "Let's start with the roadmap. Sounds good to me.",
"timestamp": "2026-10-14T09:30:12Z",
"app_name": "Weekly Sync",
"duration": 6.1,
"language": "en",
"engine": "parakeet",
"model": "parakeet-tdt-0.6b-v3",
"words_count": 9,
"recording_id": "8F8C1F45-6D03-44D2-A38C-0C4DE4F7E5F7",
"speaker_state": "ready",
"transcript_revision": "6E2F8A4C-1B3D-4C5E-8F7A-9B0C1D2E3F4A",
"transcript_version": "f74d369e3227eb16d6374c3f0f6fbccdfec89351fc3ef4023182bb86ec8ff7d6",
"speakers": [
{
"id": "S1",
"name": "Lev",
"profile_id": "D3B07384-D113-4EC6-A3B0-1F2E3D4C5B6A",
"voice_state": "linked"
},
{
"id": "S2",
"name": "Speaker 2",
"suggested_name": "Mira",
"profile_id": "A1F2C3D4-5E6F-4A7B-8C9D-0E1F2A3B4C5D",
"voice_state": "suggestion"
}
],
"speaker_segments": [
{
"start": 0,
"end": 2.4,
"speaker": "S1",
"text": "Let's start with the roadmap."
},
{
"start": 2.9,
"end": 6.1,
"speaker": "S2",
"text": "Sounds good to me."
}
]
}
],
"total": 1,
"limit": 10,
"offset": 0
}| Field | Description |
|---|---|
recording_ | Only on entries the Recorder saved for speaker detection: the same recording_id as in GET /v1/recorder/recordings and in the recording's .transcript-ready.json receipt. It stays the same after a retranscription. Copies of the entry synced to other Macs do not have it. |
speaker_ | pending, ready, or failed. |
transcript_ | Identifies the speaker detection run. Detecting speakers again creates a new revision. |
transcript_ | Changes whenever speakers or text of the transcript are corrected, for example by merging, splitting, or reassigning speakers, also within the same revision. |
speakers | Present once a speaker transcript exists, in order of first appearance. Each speaker has id (S1, S2, …), name, and voice_state, and profile_id when a voice profile is linked or suggested. name is the confirmed name or Speaker N. A name that a voice profile suggested and nobody confirmed yet is in suggested_name, while name stays Speaker N. |
speaker_ | Only with include=speaker_segments: the segments with start and end in seconds, speaker (the speaker's id), and text. |
voice_state has one of these values:
| Value | Meaning |
|---|---|
none | No voice profile is linked, and none can be created yet: the speaker has no name, several voice profiles share the name, there are less than 20 seconds of speech, or this Mac has no current voice sample or no Premium. |
canEnroll | The speaker has a name and at least 20 seconds of speech, and this Mac has a current voice sample: you can create a voice profile. Only with Premium. |
suggestion | A voice profile suggested the name in suggested_name, and it waits for confirmation. |
linked | The speaker is linked to the voice profile in profile_id. |
outdated | The linked voice profile was learned with an older speaker model and recognizes no one until you choose Relearn Voice from This Recording in the app. Only with Premium and a current voice sample; otherwise the speaker shows linked. |
Name speakers and teach voice profiles
Available from TypeWhisper 1.8.
Three routes let a script name the speakers of a History entry and teach TypeWhisper their voices, for example after it matched a meeting's speakers to the attendees. They always require the API token, even with Require API Token turned off, and paired iPhones and iPads cannot call them. Setting a name works without Premium. Teaching a voice profile and confirming a suggestion need Premium, a current voice sample of the speaker on this Mac, and at least 20 seconds of their speech. The routes never accept or return audio or voice data.
Voice profiles stay on this Mac. They are not synced, exported, or included in backups. As in the app, make sure the person agrees before you save their voice.
- Read the entry with
GET /v1/history. For a Recorder recording, take the entry whoserecording_idmatches. Wait untilspeaker_stateisready, and stop atfailed. Note the entry'sid,transcript_revision, andtranscript_versionand theidof the speaker you mean. PUT /v1/history/speakers/namesets the speaker's name in this transcript. It never teaches a voice profile, even when the name matches a suggestion. A different name unlinks the speaker from its voice profile; a change in capitalization or accents only keeps the link. On a speaker with a suggestion, any name discards it; useconfirmto accept it.POST /v1/history/speakers/enrolladds the named speaker's voice to a voice profile: toprofile_idif you send one, otherwise to the profile the speaker is linked to, or to a new profile when none has this name. If voice profiles with this name exist, the request returns 409profile_name_existswith their IDs inerror.profile_ids. Send one of them asprofile_id, or sendcreate_profile: trueto create a profile for another person with the same name. The profile's name must match the speaker's name.POST /v1/history/speakers/confirmconfirms a suggestion: send theprofile_idthat the speaker carries withvoice_statesuggestion. TypeWhisper then gives the speaker the suggested name and adds the voice to that profile.
DISCOVERY="$HOME/Library/Application Support/TypeWhisper/api-discovery.json"
SPEAKERS_URL="http://127.0.0.1:$(jq -r '.port' "$DISCOVERY")/v1/history/speakers"
(
AUTH_HEADER="$(mktemp)" || exit
trap 'rm -f "$AUTH_HEADER"' EXIT
jq -r '"Authorization: Bearer " + .token' "$DISCOVERY" > "$AUTH_HEADER"
curl -X PUT "$SPEAKERS_URL/name" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"history_id":"<history-uuid>","transcript_revision":"<revision-uuid>","transcript_version":"<version>","speaker_id":"S1","name":"Lev"}'
curl -X POST "$SPEAKERS_URL/enroll" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"history_id":"<history-uuid>","transcript_revision":"<revision-uuid>","transcript_version":"<version>","speaker_id":"S1"}'
curl -X POST "$SPEAKERS_URL/confirm" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"history_id":"<history-uuid>","transcript_revision":"<revision-uuid>","transcript_version":"<version>","speaker_id":"S2","profile_id":"<profile-uuid>"}'
)Request fields
| Field | Description |
|---|---|
history_ | The id of the History entry, not the Recorder's recording_id. |
transcript_, transcript_ | The values you read. If the transcript changed since, the request returns 409 stale_transcript. |
speaker_ | The speaker's id, such as S1. |
name | Only for name, and required there: 1 to 100 characters without control characters. |
profile_ | Optional for enroll: the profile to add the voice to. Required for confirm: the suggested profile. |
create_ | Only for enroll: true creates a new profile although profiles with this name exist. Do not combine it with profile_id. |
Send a JSON object of at most 16 KiB with only these fields. Every successful response describes the speaker's current state:
{
"history_id": "0B7A3C1E-2F4D-4E8A-9C61-3D5E7F9A1B2C",
"transcript_revision": "6E2F8A4C-1B3D-4C5E-8F7A-9B0C1D2E3F4A",
"transcript_version": "f74d369e3227eb16d6374c3f0f6fbccdfec89351fc3ef4023182bb86ec8ff7d6",
"speaker_id": "S1",
"name": "Lev",
"profile_id": "D3B07384-D113-4EC6-A3B0-1F2E3D4C5B6A",
"voice_state": "linked",
"already_learned": false
}already_learned is true when the profile already contained this voice sample. Enrollment and confirmation count each sample only once per profile, so you can retry a request, also after a restart, without counting it twice.
Errors
| Status and code | Meaning |
|---|---|
400 bad_ | The body is not a JSON object, has other fields, lacks a required field, contains an invalid UUID, or combines create_profile with profile_id. |
400 invalid_ | The name is empty, longer than 100 characters, or contains control characters. |
401 unauthorized | The request has no valid API token. |
403 premium_ | enroll or confirm without Premium. |
404 history_, speaker_ | The History entry does not exist, or the speaker is not part of its transcript. |
409 speakers_ | Speaker detection has not finished or failed, or the entry has no speaker detection. |
409 stale_ | The transcript changed since you read it. Read the entry again and check who the speaker is before you retry. |
409 profile_ | Voice profiles with this name exist; error.profile_ids lists them. |
409 profile_ | The speaker's name does not match the name of the requested profile. |
409 confirmation_, suggestion_ | enroll: the speaker has an unconfirmed suggestion, so confirm it or set a name first. confirm: the speaker no longer has this suggestion. |
409 sample_ | This voice sample was already learned, completely or in part. Correct the profile in the app. |
409 stale_ | The voice samples on this Mac do not match the transcript. Detect speakers again. |
409 profile_ | The requested or previously learned profile was deleted. Retries do not create it again. |
409 profile_ | The profile was learned with an older speaker model. Relearn it in the app. |
413 payload_ | The body is larger than 16 KiB. |
422 name_ | enroll: name the speaker first. |
422 voice_, invalid_ | This Mac has no usable voice sample of the speaker, or less than 20 seconds of their speech. |
500 history_, speaker_ | The change could not be saved or read back. Retry the same request. |
Speaker IDs are detection labels, not audio channels: S1 is not necessarily the microphone. Check which speaker is the person you mean, for example from what they say, instead of relying on the order or the invitation list. A new name does not undo what a profile learned; to correct a profile, choose Relearn Voice from This Recording or delete the profile in the app.
Dictionary
Recognition terms influence transcription. Dictionary Corrections run after transcription and rewrite matching text unless apply_corrections is false.
curl http://localhost:8978/v1/dictionary/terms
curl -X PUT http://localhost:8978/v1/dictionary/terms \
-H "Content-Type: application/json" \
-d '{"term_entries":[{"term":"TypeWhisper","ctc_min_similarity":0.65}],"replace":false}'
curl -X DELETE http://localhost:8978/v1/dictionary/terms \
-H "Content-Type: application/json" \
-d '{"term":"TypeWhisper"}'For simple terms, send a terms string array. For Parakeet CTC tuning, send term_entries with optional ctc_min_similarity. Set replace to true to replace all existing recognition terms; otherwise entries are merged.
curl http://localhost:8978/v1/dictionary/corrections
curl -X PUT http://localhost:8978/v1/dictionary/corrections \
-H "Content-Type: application/json" \
-d '{"original":"teh","replacement":"the","caseSensitive":false}'
curl -X DELETE http://localhost:8978/v1/dictionary/corrections \
-H "Content-Type: application/json" \
-d '{"original":"teh"}'From TypeWhisper 1.8, a Dictionary Correction can be a regular expression. Correction responses include isRegex. Send isRegex: true with PUT or DELETE to address a regular expression; without it, you address a literal correction. A regular expression is identified by its exact text, including case and spaces, so it can exist next to a literal correction with the same text. To switch a correction between the two, delete it and add it again. An invalid pattern returns 400. caseSensitive applies to regular expressions as well. The replacement is literal except for \, \, \, \ (a space), and \; $1 does not insert a capture group.
curl -X PUT http://localhost:8978/v1/dictionary/corrections \
-H "Content-Type: application/json" \
-d '{"original":"type ?whisper","replacement":"TypeWhisper","isRegex":true}'Settings Backup: API and CLI
The API uses the same schema as Settings > Advanced > Backup & Restore. Export includes all supported categories: workflows, Dictionary entries, snippets, prompt actions, profiles, hotkeys, non-bundled plugins, history, update channel, and preferences.
DISCOVERY="$HOME/Library/Application Support/TypeWhisper/api-discovery.json"
TYPEWHISPER_API_PORT="$(jq -r '.port' "$DISCOVERY")"
(
AUTH_HEADER="$(mktemp)" || exit
settings_backup_tmp="$(mktemp ./typewhisper-settings.json.tmp.XXXXXX)" || exit
trap 'rm -f "$AUTH_HEADER" "$settings_backup_tmp"' EXIT
jq -r '"Authorization: Bearer " + .token' "$DISCOVERY" > "$AUTH_HEADER"
curl --fail --silent --show-error \
"http://localhost:$TYPEWHISPER_API_PORT/v1/settings/export" \
-H @"$AUTH_HEADER" \
--output "$settings_backup_tmp" &&
mv "$settings_backup_tmp" typewhisper-settings.json &&
curl --fail --silent --show-error -X POST \
"http://localhost:$TYPEWHISPER_API_PORT/v1/settings/import" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
--data-binary @typewhisper-settings.json
)Import applies all categories in the document using the merge/skip rules and deletes nothing. It does not wipe the destination Mac first: duplicate Dictionary entries and snippets are skipped, occupied hotkey slots are preserved, unavailable or installed plugins are skipped, and history outside the destination retention window is omitted. From TypeWhisper 1.8, an import also skips workflows, profiles, prompt actions, and history entries that already exist unchanged, so importing a backup onto the Mac it came from duplicates nothing. Edited items are added as new ones.
From TypeWhisper 1.8, mode=replace also overwrites the first workflow, profile, or prompt action with the same name and the hotkey slots the backup contains, like Replace existing items when you import under Settings > Advanced > Backup & Restore. Profiles keep their priority and workflows their position. The default is mode=merge; any other value returns 400.
curl -X POST "http://localhost:8978/v1/settings/import?mode=replace" \
-H "Content-Type: application/json" \
--data-binary @typewhisper-settings.jsonImport response
{
"workflowsImported": 4,
"workflowsUpdated": 0,
"workflowsSkipped": 1,
"dictionaryImported": 52,
"dictionarySkipped": 3,
"snippetsImported": 6,
"snippetsSkipped": 1,
"promptActionsImported": 2,
"promptActionsUpdated": 0,
"promptActionsSkipped": 0,
"profilesImported": 2,
"profilesUpdated": 0,
"profilesSkipped": 0,
"hotkeysApplied": 1,
"hotkeysSkipped": 1,
"pluginsInstalled": 2,
"pluginsSkipped": 1,
"pluginsRegistryFetchFailed": false,
"historyImported": 120,
"historySkippedByRetention": 8,
"historySkippedAsDuplicate": 0,
"historySkippedUnreadableDestination": 0,
"updateChannelApplied": true,
"preferencesApplied": 27
}From TypeWhisper 1.8, the response also contains: workflowsUpdated, promptActionsUpdated, and profilesUpdated, the items that mode=replace overwrote; workflowsSkipped, promptActionsSkipped, and profilesSkipped, the items that already existed unchanged; historySkippedAsDuplicate, the history entries already on this Mac; and historySkippedUnreadableDestination, the history entries left out because the existing history could not be read to check for duplicates.
CLI commands
Install the CLI in Settings > Advanced > Command Line Tool. export writes the API backup atomically; import accepts relative, absolute, and tilde-expanded paths. Add --json for a machine-readable export receipt or import summary. From TypeWhisper 1.8, --replace imports with mode=replace.
mkdir -p ~/.config/typewhisper
typewhisper export ~/.config/typewhisper/settings.json
typewhisper import ~/.config/typewhisper/settings.json
typewhisper import ~/.config/typewhisper/settings.json --json
typewhisper import ~/.config/typewhisper/settings.json --replaceA backup can contain transcription history, prompts, app/website rules, and other personal configuration. Review it before committing it to a dotfiles repository. The implementation adds explicit import/export commands; it does not automatically load $XDG_CONFIG_HOME/typewhisper/config.toml.
Audio settings
Available from TypeWhisper 1.8.
GET /v1/settings/audio returns the microphone priority list and the recording audio options from Settings > Dictation. PATCH /v1/settings/audio changes any of them the way the settings window does: the change is saved, applies to the next recording, and shows up in an open settings window.
A script can record from another input for a while and then restore your setup, for example to dictate speech that another app plays into the virtual BlackHole 2ch device. Changing the macOS default input does not do this: TypeWhisper records from the first connected device in its Microphone Priority list and uses the macOS default input only when none of these devices is connected.
curl http://localhost:8978/v1/settings/audio
curl -X PATCH http://localhost:8978/v1/settings/audio \
-H "Content-Type: application/json" \
-d '{
"input_priority": [{ "id": "BlackHole2ch_UID", "name": "BlackHole 2ch" }],
"audio_ducking_enabled": false
}'
curl -X PATCH http://localhost:8978/v1/settings/audio \
-H "Content-Type: application/json" \
-d '{"input_priority":[]}'{
"input_devices": [
{
"id": "BuiltInMicrophoneDevice",
"name": "MacBook Pro Microphone",
"is_system_default": true
},
{
"id": "BlackHole2ch_UID",
"name": "BlackHole 2ch",
"is_system_default": false
}
],
"input_priority": [
{
"id": "AppleUSBAudioEngine:HyperX:QuadCast 2",
"name": "HyperX QuadCast 2"
}
],
"active_input": {
"id": "BuiltInMicrophoneDevice",
"name": "MacBook Pro Microphone"
},
"audio_ducking_enabled": true,
"audio_ducking_level": 0.2,
"pause_media_during_recording": false,
"sound_feedback_enabled": true
}Fields
| Field | Description |
|---|---|
input_ | Read-only. The input devices connected now, each with id (the CoreAudio device UID), name, and is_system_default. |
input_ | The saved Microphone Priority list, including devices that are not connected. Each entry needs an id. name is optional and is replaced by the current name while the device is connected. [] records from the macOS default input. |
active_ | Read-only. The input the next recording would use: the first connected entry of input_priority, otherwise the macOS default input. null when no input is available. |
audio_ | Lower the system volume during recording. |
audio_ | Share of the current volume kept during recording, from 0 (muted) to 1. The slider in Settings offers 0 to 0.5. |
pause_ | Pause media playback during recording. |
sound_ | Play sounds when recording starts, when a transcription succeeds, and on errors. |
Send a JSON object with only the fields you want to change. A successful PATCH returns the full state in the same shape as GET.
PATCH returns 400 and changes nothing for invalid JSON, a value of the wrong type or outside its range, an empty or duplicate device ID, a read-only field, or an unknown field. A field that only another TypeWhisper platform supports counts as unknown, and the error message lists the fields macOS accepts. While a dictation or the Recorder is recording or still processing, PATCH returns 409 and changes nothing. GET keeps working.
Change settings temporarily
Save the GET response before you change anything. To restore it, send it back without the read-only fields input_devices and active_input. Wait until the recording has been processed, because a PATCH during recording or processing returns 409.
DISCOVERY="$HOME/Library/Application Support/TypeWhisper/api-discovery.json"
TYPEWHISPER_API_PORT="$(jq -r '.port' "$DISCOVERY")"
AUDIO_URL="http://localhost:$TYPEWHISPER_API_PORT/v1/settings/audio"
(
AUTH_HEADER="$(mktemp)" || exit
trap 'rm -f "$AUTH_HEADER"' EXIT
jq -r '"Authorization: Bearer " + .token' "$DISCOVERY" > "$AUTH_HEADER"
saved="$(curl --fail --silent --show-error "$AUDIO_URL" -H @"$AUTH_HEADER")" || exit
curl --fail --silent --show-error -X PATCH "$AUDIO_URL" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{
"input_priority": [{ "id": "BlackHole2ch_UID", "name": "BlackHole 2ch" }],
"audio_ducking_enabled": false
}'
printf '%s ' "Play speech into BlackHole 2ch, dictate it, then press Return to restore your settings."
read -r _
jq 'del(.input_devices, .active_input)' <<<"$saved" | \
curl --fail --silent --show-error -X PATCH "$AUDIO_URL" \
-H @"$AUTH_HEADER" \
-H "Content-Type: application/json" \
--data-binary @-
)The CLI Tool offers the same with typewhisper audio and typewhisper audio set.
Workflow-backed rules
curl http://localhost:8978/v1/rules
curl -X PUT "http://localhost:8978/v1/rules/toggle?id=<uuid>"Each entry includes its UUID, enabled state, priority, app bundle identifiers, URL patterns, language mode and hints, and optional translation target. Use a rule UUID as workflow_id when starting dictation with a forced workflow.
Legacy compatibility: /v1/profiles and /v1/profiles/toggle remain available as exact aliases for older integrations.
Process text with language models and workflows
Available from TypeWhisper 1.8.
These routes run text through the language models and workflows of this Mac and return the result without inserting it anywhere. Local tools use them with the API token like the other routes. Requests can go to cloud providers in your language model list and incur their API costs. With Premium, paired iPhones and iPads use the same routes once Allow Writing with This Mac is turned on under Settings > Premium > iPhone and iPad. Until then they get 403 writing_not_allowed, and remote_writing_allowed in GET /v1/status is false. The setting does not affect local tools.
Language models
curl http://localhost:8978/v1/llm/models
curl -X POST http://localhost:8978/v1/llm/process \
-H "Content-Type: application/json" \
-d '{"system_prompt":"Rewrite the text as a short, friendly reply. Return only the reply.","text":"thanks got it will send the file tomorrow"}'{
"models": [
{
"provider_id": "appleIntelligence",
"provider_name": "Apple Intelligence",
"name": "Apple Intelligence",
"ready": true
},
{
"provider_id": "Claude",
"provider_name": "Claude",
"model_id": "claude-haiku-4-5",
"name": "Claude Haiku 4.5",
"ready": true
}
]
}GET /v1/llm/models lists each language model provider with its models. ready says whether the provider can be used now. Providers without a model list, such as Apple Intelligence, appear once, without model_id. POST /v1/llm/process takes system_prompt and text, both required, and returns the result as text. With provider_id, and optionally model_id, it makes one call to that provider; model_id without provider_id returns 400.
Without provider_id, TypeWhisper tries the entries of the global LLM fallback list in order, as for workflows that inherit it.
Workflows
curl http://localhost:8978/v1/workflows
jq -n --rawfile text call-notes.txt \
'{workflow_id: "<uuid>", text: $text, language: "en"}' |
curl -X POST http://localhost:8978/v1/workflows/run \
-H "Content-Type: application/json" \
--data-binary @- |
jq -r .text{
"workflows": [
{
"id": "3C2B1A09-8F7E-4D6C-B5A4-0D1E2F3A4B5C",
"name": "Meeting Notes",
"template": "meetingNotes"
}
]
}GET /v1/workflows lists the enabled workflows that can run without a text field: workflows with a prompt, and Dictation Only workflows with Inline Commands. Workflows that use Apple Translate or only an action plugin are not listed; GET /v1/rules lists all workflows. template is the ID of the template, such as meetingNotes or custom.
POST /v1/workflows/run takes workflow_id and text, both required, and optionally language, the language code of the text. It runs the workflow with its own provider and model settings and returns text, workflow_id, and workflow_name. TypeWhisper inserts nothing, does not press Enter, and runs no action plugin. A fixed output format is kept; Auto-Detect falls back to plain text because there is no app. An unknown workflow returns 404 workflow_not_found, a disabled one 409 workflow_disabled, and one that does not process text 409 workflow_not_runnable.
Limits and errors
The request body may have up to 512 KiB, system_prompt up to 32 KiB, and text up to 400 KiB. Each client runs one request at a time and up to 30 per minute, and the Mac runs at most two at once; all local tools together count as one client. A request gets its answer after 120 seconds at the latest. Error messages are short and contain no details from the provider.
| Status and code | Meaning |
|---|---|
400 bad_ | Invalid JSON, a missing field, empty text, an invalid workflow_id, or model_id without provider_id. |
403 writing_ | A paired device sent the request while Allow Writing with This Mac is off. |
403 forbidden | Managed settings refuse the language model or provider. |
404 workflow_ | The workflow does not exist. |
409 workflow_, workflow_ | The workflow is disabled, or it does not process text. |
413 payload_ | The request, system_prompt, or text exceeds its limit. |
413 context_ | The text is too long for the language model. |
429 busy, rate_ | This client already has a request running, two requests are running, or the client sent 30 requests within the last minute. |
502 llm_ | The language model failed, or its result is larger than 1 MiB. |
503 llm_ | No language model is ready. |
503 cancelled | The request was cancelled. |
504 timeout | The Mac did not finish within 120 seconds. |
Dictation control
These routes use TypeWhisper's system-wide dictation pipeline and insertion behavior. Start returns a session UUID; stop returns the same UUID so a script can poll its result.
curl -X POST http://localhost:8978/v1/dictation/start \
-H "Content-Type: application/json" \
-d '{"workflow_id":"<uuid>"}'
curl -X POST http://localhost:8978/v1/dictation/stop
curl http://localhost:8978/v1/dictation/status
curl "http://localhost:8978/v1/dictation/transcription?id=<uuid>"The start body is optional. When workflow_id is supplied, it must name an existing enabled workflow; TypeWhisper returns the selected workflow ID and name in the start response.
Poll /v1/dictation/transcription until status becomes completed or failed. A completed response contains both final and raw text plus timestamp, app context, duration, language, engine, model, and word count.
From TypeWhisper 1.8, the response for each of the last 100 dictations since TypeWhisper started also contains latency: the timings of each step from the start request to the verified insertion and the restored clipboard (fields ending in _ms in milliseconds, recording_seconds in seconds), plus the engine, the model, and how the text was inserted and verified. It never contains transcript text, audio, or app content. Other sessions have no latency field.
Report the active browser tab
Available from TypeWhisper 1.8.
Browser extensions report the active tab with PUT /v1/browser/context, so website workflows match the page you are on. This also works in the Mac App Store edition, which cannot read browser addresses itself, and in browsers without AppleScript support, such as Firefox. While a current report exists for the browser in front, TypeWhisper uses it instead of asking the browser. In the Mac App Store edition, the Website trigger appears in the workflow editor once an extension has reported.
The TypeWhisper extension for Chrome and Firefox uses this route and, like other clients, sends the API token.
curl -X PUT http://localhost:8978/v1/browser/context \
-H "Content-Type: application/json" \
-d '{"client_id":"c7d1e0f2-4b3a-4f9e-9a1d-2b6c8e0f1a3d","focused":true,"url":"https://github.com/TypeWhisper/typewhisper-mac/pulls","title":"Pull requests"}'
curl -X PUT http://localhost:8978/v1/browser/context \
-H "Content-Type: application/json" \
-d '{"client_id":"c7d1e0f2-4b3a-4f9e-9a1d-2b6c8e0f1a3d","focused":false}'{
"status": "active",
"app_bundle_id": "com.google.Chrome",
"url": "https://github.com/TypeWhisper/typewhisper-mac/pulls"
}Request fields
| Field | Description |
|---|---|
client_ | Required. A stable ID of the reporting extension, 1 to 128 characters. A report with focused: false only clears the report with the same client_id. |
focused | Required. true while the browser window has focus, false when it loses focus. |
url | The address of the active tab. Only http, https, and file addresses count as a website. Leave it out for a page without a website, such as a new tab; other addresses, such as chrome://settings, do not count as a website either. |
title | The title of the tab. TypeWhisper trims it and keeps up to 512 characters. |
A focused report replaces the previous one and belongs to the app in front when it arrives. TypeWhisper ignores it while TypeWhisper itself, no app, or an app that cannot open web pages is in front. Reports expire after three minutes, so repeat the report every minute while the browser stays in front.
status is active when TypeWhisper stored the report for the app in app_bundle_id, ignored when it did not, and cleared for focused: false. url is left out when the tab shows no website. Invalid JSON or a missing or wrongly typed client_id or focused returns 400 with Invalid JSON body, and so does an empty client_id or one longer than 128 characters, with Missing or invalid 'client_id'.
Recorder control
Recorder creates a saved recording and can capture microphone, system audio, or both. It follows the same mixing, finalization, and optional transcription path as the Recorder UI without pasting text into another app.
curl -X POST "http://localhost:8978/v1/recorder/start?mic=true&system_audio=true"
curl -X POST http://localhost:8978/v1/recorder/stop
curl http://localhost:8978/v1/recorder/status
curl "http://localhost:8978/v1/recorder/session?id=<uuid>"mic and system_audio accept true, false, 1, or 0. Omitted values inherit the current Recorder settings, and at least one resolved source must remain enabled.
{
"id": "8F8C1F45-6D03-44D2-A38C-0C4DE4F7E5F7",
"status": "completed",
"text": "Meeting notes from the recording.",
"output_file": "/Users/alex/Documents/TypeWhisper Recordings/Recording.m4a"
}Sessions move through recording, finalizing, and completed or failed. text is omitted when transcription is disabled or empty; output_file is returned when a recording was finalized, and failed sessions include error.
Completed Recorder transcripts
Available from TypeWhisper 1.8.
GET /v1/recorder/recordings returns the latest successfully saved transcript of each recording, whether the recording was started manually, by the calendar integration, or through the API. The results survive app restarts and do not depend on live preview or an API session, so an integration can catch up on transcripts it missed.
curl "http://localhost:8978/v1/recorder/recordings?since=1791100000.123456"
curl "http://localhost:8978/v1/recorder/recordings?since=2026-10-04T07:46:40Z"{
"recordings": [
{
"source": "recorder",
"recording_id": "8F8C1F45-6D03-44D2-A38C-0C4DE4F7E5F7",
"completion_id": "59CBC40C-274A-47A9-804C-8774AB504401",
"completed_at": 1791100000.123456,
"text": "Meeting notes from the recording.",
"audio_file": "/Users/alex/Documents/TypeWhisper Recordings/Weekly Sync.m4a",
"transcript_file": "/Users/alex/Documents/TypeWhisper Recordings/Weekly Sync.txt",
"markdown_file": "/Users/alex/Documents/TypeWhisper Recordings/Weekly Sync.transcript.md"
}
]
}Response fields
source– Alwaysrecorder.recording_id– Identifies the recording and stays the same after a retranscription.completion_id– Identifies one successful save. Each retranscription gets a new one.completed_at– Time of the successful save in Unix seconds, with fractional seconds.text– The saved transcript.audio_file,transcript_file– Paths of the audio file and the plain-text transcript. Both files exist when the response is built.markdown_file– Path of the Markdown transcript, included only when one was saved and still exists. Currently, calendar recordings with meeting metadata get one.
since accepts Unix seconds or an ISO 8601 timestamp with a time zone, such as 2026-10-04T07:46:40Z, and filters inclusively by the time a transcript was saved, not by the time the recording started. Without since, the response lists every recording with a saved transcript. Results are sorted by completed_at, oldest first.
To process each transcript once, keep the completed_at of the last result you handled and pass it as since in the next request. Because the filter is inclusive, that result comes back, together with any others saved at the same time. Skip the completion_id values you have already processed.
A successful retranscription keeps the recording_id, assigns a new completion_id, and replaces the earlier result. The list holds the latest result of each recording, not every revision, and a failed attempt leaves the last successful result in place. Recordings without a saved transcript are not listed. Recordings made before TypeWhisper 1.8 appear after their next successful transcription.
TypeWhisper saves a completion receipt next to each recording, such as Weekly Sync.m4a.transcript-ready.json, and deletes it when you delete the recording in TypeWhisper. If a receipt cannot be read, the API returns 500 instead of leaving the recording out, so a consumer does not move its cursor past it. An invalid since value returns 400.
Webhooks, scripts, and plugin events
To act on a transcript as soon as it is saved, use one of the following. Both add-on options are off by default, also for webhooks and scripts you set up earlier.
- Webhook – With "Also send completed Recorder transcripts" turned on, a webhook receives each saved transcript as JSON with the fields above. Requires version 1.2.0 or newer of the add-on.
- Script Runner – With "Also run for completed Recorder transcripts" turned on, a script receives the transcript through stdin and the file paths in environment variables. Requires version 1.2.0 or newer of the add-on.
recorderTranscriptReady– The Plugin SDK event is sent after the transcript and its receipt are saved, including after a retranscription and with live preview turned off. It is separate fromtranscriptionCompleted, so plugins that handle dictations do not receive Recorder transcripts.
Webhooks, scripts, and plugins receive transcripts only while TypeWhisper and the add-on are running, and delivery is best effort. Use GET /v1/recorder/recordings to catch up on anything they missed.
Error responses
Failures use a nested JSON error object with code and message fields. Handle the HTTP status first and use message for detail:
{
"error": {
"code": "unauthorized",
"message": "Missing or invalid API token"
}
}Common HTTP statuses
400– Missing or invalid body, query parameter, file, audio format, workflow ID, or option value.401– The token is required (the default from TypeWhisper 1.8, and always for the speaker routes) and the request has no valid Bearer or X-TypeWhisper-API-Token value.403– The request came from a browser on behalf of another website, managed settings refuse the requested engine, model, or provider, a Premium feature such as speaker detection is not available, or a paired device may not write with this Mac (all from TypeWhisper 1.8).404– A requested history entry, speaker (from TypeWhisper 1.8), rule, workflow, dictation session, Recorder session, engine, or model does not exist.409– The request conflicts with the current state: a recording is already running or still processing, an overridden engine is not configured, or an engine cannot load, unload, or delete the requested model. From TypeWhisper 1.8 also: a workflow is disabled or does not process text, or a speaker request conflicts with the current transcript or voice profiles.413– The request body exceeds the 256 MiB upload limit. From TypeWhisper 1.8 also: the body exceeds the smaller limit of the text processing or speaker routes, or the text is too long for the language model.422– Speaker detection or a voice profile request cannot proceed: the engine returned no timestamps, the speaker has no name yet, or there is no usable voice sample (from TypeWhisper 1.8).429– A text processing request is already running, or too many arrived within a minute (from TypeWhisper 1.8).501– Target-language translation was requested on a macOS version before 15.502– The language model failed or returned more than 1 MiB (from TypeWhisper 1.8).503– No transcription engine is selected, or a newer request superseded a translation; retry it. From TypeWhisper 1.8 also: speaker detection or a language model is not available, or text processing was cancelled.504– Text processing did not finish within 120 seconds (from TypeWhisper 1.8).500– An internal transcription, Dictionary, settings backup, session, or Recorder receipt error occurred. Check the app logs or diagnostics export for detail.