API documentation
REST endpoints, authentication, and code examples for the waven.ai API. Every request goes to https://waven.ai.
This page is the guided tour. The full reference — every public route the backend exposes, generated from its OpenAPI spec, with request/response schemas and curl / JS / Python samples — is at /docs/reference.
All API requests require a Bearer token (Clerk JWT or API key).
# Using Clerk JWT curl -H "Authorization: Bearer <clerk_jwt>" \ https://waven.ai/api/v1/user # Using API key curl -H "Authorization: Bearer wvn_xxxx_..." \ https://waven.ai/api/v1/models
Keys are issued as wvn_<4 chars>_<secret> and shown once, at creation. Keys issued before the waven rename start with tts_ and keep working; new keys are always wvn_. Requests larger than 200 MB are rejected at the edge with a 413.
Generate speech from text. Returns audio metadata and a download URL.
| Param | Type | Description |
|---|---|---|
| text * | string | Text to synthesize (max 5,000 chars) |
| model * | string | Model name (omnivoice, kokoro) |
| language | string | Language code (default: "Auto") |
| speaker | string | Speaker ID (model-dependent) |
| speed | float | Speed multiplier (0.25 – 4.0) |
| ref_id | string | Reference audio ID from /upload-ref (voice cloning) |
| gallery_voice_id | string | Saved gallery voice ID (alternative to ref_id) |
| format | string | wav, mp3, or ogg (default: wav) |
curl -X POST https://waven.ai/api/v1/generate \
-H "Authorization: Bearer wvn_xxxx_..." \
-F "text=Hello world" \
-F "model=omnivoice" \
-F "speed=1.0"
# Response:
{
"id": "abc123",
"generation_id": 4711,
"model": "omnivoice",
"file": "/api/v1/audio/<user_id>/omnivoice_abc123.wav",
"inference_time": 1.23,
"total_time": 1.48,
"duration": 2.5,
"rtf": 0.492,
"sample_rate": 22050
}Pass at most one of reference_audio, ref_id, gallery_voice_id — sending more than one is a 400. Fetch file with the same Authorization header to download the audio.
Long-form synthesis (up to 50,000 chars). Automatically chunks text at sentence boundaries.
Same parameters as /generate, plus: chunk_max_chars (default 500), pause_ms (default 150), allow_partial (default false).
Two response shapes: the default queues the work and returns 202 with {"job_id", "status": "pending"} — poll GET /api/v1/generate-long/jobs/{job_id}. With allow_partial=true it runs inline and returns 200 with the stitched audio.
List available models with their status, default voice, and languages. Stock voices live at /api/v1/voices below.
The stock voice catalogue — no API key needed. Each entry carries id, label, display_name, language, locale, gender, engine, tags and a preview_url (a short pre-rendered sample in that voice, or null until it has been rendered). Filter with ?language=es. slots maps fast / default / clone to the engine serving each. Pass a voice id as speaker on /generate. Cloned voices are your own and live under /api/v1/voice-gallery.
curl https://waven.ai/api/v1/voices?language=en | jq '.voices[0]'
{
"id": "af_heart",
"label": "Heart — US English, female",
"display_name": "Heart",
"language": "en",
"locale": "en",
"gender": "female",
"engine": "kokoro",
"tags": ["default"],
"preview_url": "/api/v1/voices/af_heart/preview?v=1"
}Get current user profile, tier, credits, subscription status, and usage stats.
Usage history requires account:read for scoped API keys. Set granularity=day|week|month (default day) and days from 1 to 365 (default 90). Invalid parameters return 422. Windows include today in UTC; weeks start Monday and months on the first. Every intersecting bucket is returned, including zero usage; the first and last week or month may be partial.
Responses are cached for 10 minutes, with a fresh window at UTC midnight. from, to, and bucket dates are ISO calendar dates. Audio-intelligence usage currently has only a billing-cycle total and is unavailable as a historical series.
curl "https://waven.ai/api/v1/user/usage/timeseries?granularity=day&days=1" -H "Authorization: Bearer wvn_xxxx_..."
{
"granularity": "day",
"days": 1,
"from": "2026-09-05",
"to": "2026-09-05",
"attribution_since": "2026-09-05",
"buckets": [
{
"date": "2026-09-05",
"tts_minutes": 2,
"stt_minutes": 3,
"tts_count": 1,
"stt_count": 2
}
]
}Per-key TTS/STT minutes and request counts. Requires the same account scope;days is 1–365, default 30. Includes keys with zero usage. api_key_id matches the key list'sid; key metadata includes name, prefix and last-used time (naive UTC).
The api_key_id: null item is unattributed: dashboard, apps, legacy usage, and deleted or revoked keys. attribution_since states when attribution began; earlier rows cannot identify which key was used. Revocation moves history into this bucket after the cached response expires (up to 10 minutes).
{
"days": 30,
"attribution_since": "2026-09-05",
"items": [
{
"api_key_id": "22222222-2222-4222-8222-222222222222",
"name": "production",
"key_prefix": "wvn_abcd",
"last_used_at": "2026-09-05T12:00:00",
"tts_minutes": 2,
"stt_minutes": 3,
"tts_count": 1,
"stt_count": 2
},
{
"api_key_id": null,
"name": "unattributed",
"key_prefix": null,
"last_used_at": null,
"tts_minutes": 1,
"stt_minutes": 0,
"tts_count": 1,
"stt_count": 0
}
]
}Manage API keys for programmatic access.
| GET /api/v1/keys/ | List your API keys (paginated: limit, offset) |
| POST /api/v1/keys/?name=my-key | Create a new key. Optional expires_in_days (1–730) |
| DELETE /api/v1/keys/{key_id} | Revoke a key |
The secret is returned once, in the create response — it is stored hashed and cannot be read back. Keys per tier: Free=1, Personal=2, Pro=5, Studio=10, Enterprise=50.
New keys expire after 365 days unless you pass expires_in_days; the list response carries expires_at and a derived status of active, expiring, expired or revoked. We email you before a key lapses. Keys issued before this was introduced have a null expires_at and keep working indefinitely. Deleting a key revokes it: it stops authenticating at once, frees a slot against your tier cap, and is hidden from the list (pass include_revoked=true to see it).
A key can be limited to a subset of the API. Pass scopes when you create one — repeat the parameter or comma-join it — and the key can only reach the routes those scopes cover. Anything else answers 403 with error.code=insufficient_scope and a WWW-Authenticate header naming what was missing.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", error_description="This API key is missing the required scope: stt:stream", scope="stt:stream"
{"detail":"This API key is missing the required scope: stt:stream","error":{"code":"insufficient_scope","required_scopes":["stt:stream"]}}Refusals report required_scopes; they never list the key's granted permissions. Unrestricted-only refusals add unrestricted_required: true and use the challenge Bearer error="insufficient_scope", error_description="unrestricted key required".
curl -X POST "https://waven.ai/api/v1/keys/?name=reader&scopes=jobs:read&scopes=account:read" \ -H "Authorization: Bearer <clerk_jwt>"
| tts:generate | POST /generate and /generate-long, plus the TTS model list and health. |
| tts:stream | The TTS streaming WebSocket. |
| stt:transcribe | Submit and cancel transcriptions, plus the STT model list and health. |
| stt:stream | The STT streaming WebSocket. |
| stt:diarize | The diarize route, and diarize=true on a submitted job. Needed alongside stt:transcribe. |
| jobs:read | Poll async jobs and download finished transcripts. Read-only: it never submits. |
| voices:read | List and download your saved voices and voice clones. |
| voices:write | Upload reference audio, save, rename, replace and delete voices. |
| account:read | Profile, usage, referral, generation history and the dictation profile. |
| webhooks:manage | Full CRUD over webhook endpoints, their secrets and their delivery log. |
Omit scopes and the key is unrestricted — it can do everything your account can. Every key issued before scopes existed is unrestricted and keeps working unchanged; the list response reports scopes: null for those. An unknown scope name is rejected with a 422 rather than being ignored, so a key is never quietly wider than you asked for. Managing keys, billing and account settings always requires an unrestricted key: a limited key cannot mint itself a wider one. WebSocket routes enforce the same scopes and close with 4003. Tickets minted by API keys remain bound to that key: connection rechecks its current permissions and rejects a revoked, expired or missing source key with close 4001.
Sandbox keys
Create a wvt_ test key in the dashboard's Sandbox section, or call POST /api/v1/keys/?sandbox=true with your signed-in session or unrestricted production key. You can keep 2 sandbox keys, separate from your plan's production key limit.
Speech, transcription, diarization, streaming, and async jobs return fixed fixtures without using your quota or creating charges. Audio is a short 1.6-second tone at 24 kHz; transcripts and audio intelligence are canned examples. The OpenAI-compatible endpoints preserve their normal audio, JSON, text and caption formats. Responses carry X-Waven-Sandbox: true and JSON bodies include "sandbox": true. Fixture jobs finish immediately and can be polled for one hour; they send no webhooks. Lists retain the latest 50 submissions.
Free-tier request limits apply on every plan, including job reads and cancellation, in a separate budget from your production keys. Expiry, revocation and scopes work as usual; scopes: null means unrestricted within the sandbox. Sandbox keys can only call the fixture and read-only routes listed below, plus the ticket endpoint for browser streaming, as authenticated sandbox principals. Other routes that authenticate API keys return403 sandbox_key_not_permitted (WebSocket close 4003), even with unrestricted scopes or admin credentials. Public endpoints ignore presented keys and keep their normal responses and redirects. The sandbox header marks fixture responses; ordinary public and account metadata retain their existing headers. Model and health fixtures are static; no containers are probed.
POST /api/v1/mcpPOST /api/v1/generatePOST /api/v1/generate-longPOST /api/v1/stt/transcribePOST /api/v1/stt/transcribe/diarizePOST /api/v1/stt/transcribe/jobsWS /api/v1/tts/streamWS /api/v1/stt/transcribe/streamPOST /api/v1/audio/transcriptionsPOST /api/v1/audio/speechPOST /v1/audio/transcriptionsPOST /v1/audio/speechGET /api/v1/generate-long/jobs/{job_id}GET /api/v1/stt/transcribe/jobsGET /api/v1/stt/transcribe/jobs/{job_id}DELETE /api/v1/stt/transcribe/jobs/{job_id}GET /api/v1/stt/transcribe/jobs/{job_id}/transcript.{ext}GET /api/v1/audio/{user_id}/{filename}GET /api/v1/userGET /api/v1/user/usageGET /api/v1/me/dictation-profileGET /api/v1/modelsGET /api/v1/tts/healthGET /api/v1/stt/healthGET /api/v1/voicesGET /api/v1/consentGET /api/v1/consent/GET /api/v1/consent/onboarding/statusPOST /api/v1/ws-tickets
The /v1/audio/transcriptions and /v1/audio/speechaliases use the same fixture routes. Audio and job downloads only serve your fixtures. Parameters use the native endpoint validators, including OmniVoice language names such as Spanish. Each job submission receives a fresh id, even with identical options. Fixture jobs reject stored strings over 256 bytes and payloads over 4 KiB before spending your sandbox request budget.
curl -X POST "https://waven.ai/api/v1/generate" \ -H "Authorization: Bearer wvt_xxxx_..." \ -F model=kokoro -F speaker=af_heart -F text="Test my integration"
Upload reference audio for voice cloning. Requires consent attestation.
| Param | Type | Description |
|---|---|---|
| reference_audio * | file | Audio file (WAV, FLAC, OGG, MP3, M4A; max 10 MB) |
| reference_text | string | Transcript of the reference audio |
| consent_attested * | boolean | Must be "true" |
Max 20 references per user. Cached for 7 days — save a voice to the gallery to keep it longer. Returns a ref_id to pass to /generate.
| Tier | Requests/min | Concurrent TTS | Concurrent STT | API keys | TTS quota | STT quota |
|---|---|---|---|---|---|---|
| Free | 10 | 1 | 1 | 1 | 60 min | 90 min |
| Personal | 20 | 1 | 1 | 2 | 300 min | 900 min |
| Pro | 60 | 3 | 2 | 5 | 3,000 min | 6,000 min |
| Studio | 120 | 5 | 4 | 10 | 7,200 min | 18,000 min |
| Enterprise | 300 | 10 | 8 | 50 | Custom | Custom |
Every metered response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds) and a RateLimit-Policy line naming both the per-minute and the concurrent dimension; a 429 adds Retry-After and a body error.code of rate_limit_rpm or rate_limit_concurrency. STT concurrency runs a slot behind TTS on the larger tiers because each transcription can hold a GPU longer than a narration request. Quotas are monthly minutes and reset on your billing date; Enterprise limits are set per contract. Paid plans can opt into pay-as-you-go to keep serving past the quota instead of 402-ing.
STT API
Transcribe an audio file. Supports auto model selection with fallback chain.
| Param | Type | Description |
|---|---|---|
| file * | file | Audio file (WAV, MP3, FLAC, OGG, M4A) |
| model | string | Model: auto, parakeet, voxtral (default: auto) |
| language | string | Language code (optional, auto-detect if omitted) |
| correct | boolean | Post-process with grammar correction LLM |
| hotwords | string | Comma-separated terms to bias the decoder toward |
| redact_pii | boolean | Mask personal data in the transcript |
| redact_entities | string | Comma-separated entity types to mask |
| granularity | string | segment (default) or word — also return per-word timestamps under words |
| format | string | json (default), txt, srt or vtt — captions/text as a file download |
| summary | boolean | Return a summary in audio_intelligence |
| sentiment | boolean | Return per-segment sentiment |
| topics | boolean | Return detected topics |
| entities | boolean | Return extracted named entities |
| translate_to | string | Translate the audio into an enabled target language (WAV/MP3, or server-decodable audio; 4 min max) |
Translation is opt-in; check /stt/models for enabled translation_targets. The JSON response keeps the source text and adds translation, translation_language, and translation_status: ok, refused, failed, or skipped. A failed translation leaves the transcript available. With redact_pii=true, both texts are masked independently; mask_map describes the source text only.
curl -X POST https://waven.ai/api/v1/stt/transcribe \ -H "Authorization: Bearer wvn_xxxx_..." \ -F "[email protected]" \ -F "model=auto" # Response: { "text": "Hello, this is a test recording.", "segments": [{"start": 0.0, "end": 2.5, "text": "Hello, this is a test recording."}], "model": "parakeet", "language": "en", "duration_seconds": 2.5, "redacted": false, "mask_map": [], "granularity": "segment", "words": null }
segments are caption-shaped cues (sentence breaks, up to two caption lines or seven seconds each). Send granularity=word to also receive words: a list of {word, start, end, confidence}. Word timing is Parakeet-only — on a Voxtral route the request still succeeds with words: null, and granularity always echoes what was actually served (word, segment, or none when the engine returned no timing). With redact_pii, masked spans are redacted in text, segments and words alike. Very long recordings fall back to segment instead of truncating the word list.
The four audio-intelligence flags add an audio_intelligence object to the response and require a paid plan — on the Free tier the transcript is still returned and each requested task is named in audio_intelligence.errors as tier_not_entitled. Each paid plan includes a monthly allowance of analysed audio-minutes; past it, requests are metered at $0.01/min (flat per request, however many flags are set) when pay-as-you-go is on, and otherwise degrade the same way with allowance_exhausted. Uploads over a few minutes are chunked server-side; for long files prefer the jobs API below, which survives a dropped connection.
Transcribe with speaker diarization. Runs ASR + Pyannote in parallel, then merges speaker labels.
| Param | Type | Description |
|---|---|---|
| file * | file | Audio file |
| model | string | ASR model (default: auto → parakeet) |
| num_speakers | int | Exact speaker count, 1–50 (overrides min/max) |
| min_speakers | int | Minimum speaker count, 1–50 |
| max_speakers | int | Maximum speaker count, 1–50 |
| hotwords | string | Comma-separated terms to bias the decoder toward |
| redact_pii | boolean | Mask personal data in the transcript |
| format | string | json (default), txt, srt or vtt — cues carry SPEAKER_XX: prefixes |
| summary | boolean | Return a summary in audio_intelligence |
Returns segments with a speaker label, plus num_speakers and low_confidence_merge (true when the ASR and diarization timelines aligned poorly). summary requires a paid plan; on the Free tier the diarized transcript is still returned and the refusal is named in audio_intelligence.errors.
Real-time streaming transcription via WebSocket. Auth: mint a single-use ticket via POST /api/v1/ws-tickets and connect with ?ticket=<ticket>. The deprecated?token=JWT transport is off by default.
// 0. Connect (ticket from POST /api/v1/ws-tickets):
wss://waven.ai/api/v1/stt/transcribe/stream?ticket=<ticket>
// 1. Send config ("redact_pii" and "redact_entities" are optional):
{"model": "voxtral", "language": null, "format": "pcm_f32le",
"redact_pii": true, "redact_entities": "EMAIL,PHONE"}
// 2. Send binary audio frames (16kHz mono float32)
// 3. Receive partial results:
{"type": "partial", "text": "Hello world"}
// 4. Send end signal:
{"type": "end"}
// 5. Receive final result:
{"type": "final", "text": "Hello, world."}
// Or, on a redact_pii session:
{"type": "final", "text": "Call [REDACTED:PHONE].",
"redacted": true, "mask_map": [{"kind": "PHONE", "start": 5, "end": 17}]}redact_pii masks partials as well as the final, and adds redacted + mask_map to the final frame only — a session that does not request redaction gets the two-field final unchanged. Because a partial is cumulative and revisable, one withholds any trailing run that could still grow into an entity, so its tail can lag the audio by a word or two. Clients that apply their own text expansions or replacements to received frames must not run them on a redacting session — an expansion around a [REDACTED:KIND] token can reintroduce the masked text.
Submit audio for queued batch transcription. Returns a job ID immediately. Poll with GET.
| Param | Type | Description |
|---|---|---|
| file * | file | Audio file |
| model | string | ASR model (default: auto) |
| language | string | Language code |
| correct | boolean | Grammar correction |
| hotwords | string | Comma-separated terms to bias the decoder toward |
| redact_pii | boolean | Mask personal data in the transcript |
| granularity | string | segment (default) or word — the finished result then carries words |
| summary | boolean | Return a summary in audio_intelligence |
| sentiment | boolean | Return sentiment in audio_intelligence |
| topics | boolean | Return topics in audio_intelligence |
| entities | boolean | Return named entities in audio_intelligence |
| translate_to | string | Translate the audio into an enabled target language (WAV/MP3, or server-decodable audio; 4 min max; plain transcription only) |
| diarize | boolean | Queue a speaker-diarization job instead (result arrives under diarization) |
| num_speakers | integer | Exact speaker count hint, 1-50 (diarize only) |
| min_speakers | integer | Lower bound on speakers, 1-50 (diarize only) |
| max_speakers | integer | Upper bound on speakers, 1-50 (diarize only) |
translate_to adds the same translation, translation_language, and translation_status fields to the finished result and completion webhook. Translation remains limited to four minutes on the job endpoint. Queued translations report skipped if the target is disabled before processing. diarize=true cannot be combined with translation.
curl -X POST https://waven.ai/api/v1/stt/transcribe/jobs \ -H "Authorization: Bearer wvn_xxxx_..." \ -H "Idempotency-Key: 6f1c0b2e-1f2a-4a1e-9f0b-2c3d4e5f6a7b" \ -F "[email protected]" # Response: {"job_id": "8c1f...", "status": "pending"}
Send an Idempotency-Key header to make a retried submit re-attach to the job the first attempt created instead of queueing a duplicate. The key is bound to the request's options, so reusing one with a different diarize or speaker hint returns 422 rather than the wrong job.
With diarize=true the job comes back with kind: "stt_diarize" and its transcript under diarization (speaker-labelled utterance runs, num_speakers, low_confidence_merge) — result stays null, so a client that never sends the flag is unaffected. Queued diarization has a tighter length ceiling than plain transcription (one pyannote pass, no chunking) and runs one at a time, so a submission can wait behind another before it starts.
List your transcription jobs, newest first. Paginated: limit (1–100, default 20), offset.
Get job status and result. Status runs pending → processing → completed | failed, with retrying between attempts, cancelled after a DELETE, and dead_letter once retries are exhausted. The finished transcript arrives under result — or under diarization when kind is stt_diarize. Per-word timestamps (granularity=word) are served here only — the list route and the transcribe.job.completed webhook carry words: null; the webhook adds words_available so a receiver knows to fetch them.
Download a completed job's transcript as a file. The extension picks the format: .srt, .vtt, .txt or .json. Same cues as the JSON result; format=srt|vtt|txt on the synchronous /transcribe and /transcribe/diarize routes returns the identical bytes inline. Captions need timings: a transcript without them (Voxtral) answers 422 — ask for .txt or .json instead. A job that is not yet complete answers 409.
curl -O -J https://waven.ai/api/v1/stt/transcribe/jobs/8c1f.../transcript.vtt \ -H "Authorization: Bearer wvn_xxxx_..." # WEBVTT # # 00:00:00.000 --> 00:00:02.500 # SPEAKER_00: Hello, this is a test recording.
Cancel a transcription job. Queued jobs leave the queue; a job already on a GPU is marked cancelled and its result discarded. Returns 204 — including for a job that already finished, so a retry is safe. An unknown or someone else's job is a 404.
curl -X DELETE https://waven.ai/api/v1/stt/transcribe/jobs/8c1f... \ -H "Authorization: Bearer wvn_xxxx_..." # 204 No Content
List available STT models with capabilities and supported languages.
SDKs and CLI
Use the Python or TypeScript client for typed API operations, automatic retries, job polling and webhook signature verification. The Python package also includes the waven command.
pip install "waven[cli]"from waven import WavenClient
from waven.api.voices import list_voices
with WavenClient(api_key="wvn_xxxx_...") as client:
voices = list_voices.sync(client=client.generated)npm install @waven/sdkimport { WavenClient } from "@waven/sdk";
const client = new WavenClient({ apiKey: "wvn_xxxx_..." });
const { data, error } = await client.api.GET("/api/v1/voices");waven config set-key
waven transcribe recording.wav --correct --json
waven transcribe recording.wav --srt
waven speak "Hello from Waven" --out hello.wav
waven jobs list
waven voicesThe CLI reads --api-key first, then WAVEN_API_KEY, then its private config file. Set WAVEN_BASE_URL to use another server.
Long recordings use the transcription jobs endpoint and wait for the result. Audio formats whose duration cannot be measured locally also use jobs. Progress goes to stderr so JSON and captions can be redirected to a file.
Clients send X-Waven-Source automatically: waven-python, waven-node or waven-cli. Requests use your existing API key scopes, quotas and billing.
MCP server
Connect an MCP client using stateless Streamable HTTP and an API key bearer header. Availability is controlled by the server operator; a disabled server returns 503.
https://waven.ai/api/v1/mcp
Authorization: Bearer wvn_xxxx_...{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}| Tool | Required scope | Description |
|---|---|---|
| transcribe_audio | stt:transcribe | Transcribe inline audio; supports language, correction, PII redaction and hotwords. |
| synthesize_speech | tts:generate | Generate speech with an optional stock voice; receive audio or a download URL. |
| list_voices | voices:read | Read the stock voice catalogue and preview URLs. |
| get_job | jobs:read | Read the status and result of your transcription job. |
| analyze_audio | stt:transcribe | Request summary, sentiment, topics or entities; requires a paid plan and audio intelligence allowance. |
Audio input is base64 only. Larger recordings use the transcription jobs endpoint. Large speech results include an absolute download URL that needs the same bearer header.
Tool calls use the same consent, quota and rate limits as the REST API. Sandbox keys return fixture audio and transcripts. There is no cloning tool. OAuth is not supported.
OpenAI-compatible API. Point the official openai SDK (Python, Node, or any client that speaks the same wire format) at https://waven.ai/v1 with a waven API key and it works with no other change. The same two endpoints also live at https://waven.ai/api/v1 — that is the canonical path, and /v1 is an alias for it, so either base_url is fine.
These routes are the same engines, quotas, rate limits and billing as the native ones — they call them internally rather than reimplementing them — with one deliberate difference: your stored dictation profile is never applied, so you always get raw engine output.
from openai import OpenAI
client = OpenAI(
api_key="wvn_xxxx_...",
base_url="https://waven.ai/v1",
)
# Speech-to-text
text = client.audio.transcriptions.create(
model="auto",
file=open("meeting.wav", "rb"),
).text
# Text-to-speech
audio = client.audio.speech.create(
model="kokoro",
voice="af_heart",
input="Hello from waven.",
response_format="mp3",
)
audio.write_to_file("hello.mp3")Support matrix
Nothing on this endpoint is silently ignored. Every parameter is either honoured or answered with a 400 naming what to send instead.
| Param | Endpoint | Verdict | Notes |
|---|---|---|---|
| file | transcriptions | accepted | The audio to transcribe. |
| model | transcriptions | accepted | auto | parakeet | voxtral. OpenAI names (whisper-1) are rejected unless an operator opts in to aliasing. |
| language | transcriptions | accepted | ISO-639-1 hint. Routing drops English-only engines when a non-English hint is present. |
| prompt | transcriptions | approximated | Mapped to keyword biasing. Split on commas and newlines only and capped at 50 terms, so a prose prompt becomes ONE term, not a term set. |
| response_format | transcriptions | accepted | json | text | srt | vtt | verbose_json. srt/vtt/text are served as text/plain, as OpenAI serves them. |
| timestamp_granularities[] | transcriptions | accepted | segment (default) or word. Also accepts timestamp_granularities without brackets. word requires response_format=verbose_json and an engine with alignments — 400 on voxtral. |
| temperature | transcriptions | rejected | Decoding is deterministic. Omit it, or send 0. |
| stream | transcriptions | rejected | 400 for true; false or omitted is accepted. Use the WebSocket route for realtime. |
| include[] | transcriptions | rejected | 400 for nonempty values, also for the unbracketed include spelling. Selects Whisper-internal logprobs this API has no equivalent for. |
| redact_pii | transcriptions | rejected | Unavailable on the shim. Use /api/v1/stt/transcribe with redact_pii=true for PII redaction. |
| (unknown field) | transcriptions | rejected | 400 naming the unknown multipart fields and listing all accepted fields. Only the names in the multipart field list are recognised. |
| model | speech | accepted | auto | kokoro | omnivoice. |
| input | speech | accepted | Text to synthesize, up to the standard 5,000-character cap (above OpenAI's 4,096). |
| voice | speech | accepted | Kokoro: a stock id from GET /api/v1/voices. OmniVoice: an owned id from GET /api/v1/voice-gallery, resolved to reference audio. Unknown or wrong-engine voices return 400 before generation; suspended gallery voices remain 403. |
| response_format | speech | accepted | wav | mp3 | ogg. opus/aac/flac/pcm are rejected, not substituted. |
| speed | speech | accepted | 0.25–4.0 when given. Omitted means the engine default; SSML prosody is honoured. An explicit speed overrides SSML. |
| instructions | speech | accepted | Mapped to OmniVoice style tags (fixed allowlist). A verified no-op on kokoro. |
| stream_format | speech | rejected | 400. This endpoint returns the complete clip in one response. |
| (unknown field) | speech | rejected | 400 naming the field. The JSON body is a closed set. |
| Param | Type | Description |
|---|---|---|
| multipart fields (STT) | form | file, model, language, prompt, response_format, temperature, timestamp_granularities[], timestamp_granularities, stream, include[], include |
| model (STT) | string | auto | parakeet | voxtral |
| model (TTS) | string | auto | kokoro | omnivoice |
| response_format (STT) | string | json | text | srt | vtt | verbose_json |
| response_format (TTS) | string | wav | mp3 | ogg — opus, aac, flac, pcm are rejected |
OpenAI model names (whisper-1, tts-1, gpt-4o-mini-tts) answer 400 with the accepted names rather than being quietly mapped onto a different engine.
- A single trailing slash on either URL shape redirects with 307 to the canonical /api/v1/audio/... route, preserving the POST method and body.
- Unknown multipart fields are rejected with 400. PII redaction (redact_pii) requires the native /api/v1/stt/transcribe route.
- Body-size refusals (413), IP throttles (429) and unexpected server errors (500) also use the OpenAI error envelope.
- Multilingual requests (voxtral) return text with no alignments at all, so segments, words and the srt/vtt formats are unavailable — the caption formats answer 400 rather than returning an empty file.
- verbose_json.duration is the length of the audio, not the time the request took.
- Whisper-internal per-segment fields (seek, tokens, avg_logprob, no_speech_prob) are omitted rather than fabricated.
- The account's stored dictation profile is never applied on these routes — they always return raw engine output.
- Usage is metered, rate-limited and billed exactly as the native routes; calls show up under the openai source in your usage breakdown.
Errors use OpenAI's envelope on these two paths only — {"error": {"message", "type", "param", "code"}}, with invalid_api_key on 401, insufficient_quota on 402 and rate_limit_error on 429, and the Retry-After / RateLimit-* headers intact. Every other route keeps its detail body.
Your account-wide dictation profile: custom vocabulary, ordered literal replacements, and the disfluency toggles. One server-side store shared by the web, macOS, and iOS clients — and the same profile the transcription routes apply by default.
| GET /api/v1/me/dictation-profile | Read the profile. Never 404s — an account that has never saved one gets the defaults with updated_at: null. |
| PUT /api/v1/me/dictation-profile | Replace it. Full-document, last-write-wins — there is no PATCH, so always send every field. |
| Param | Type | Description |
|---|---|---|
| vocabulary | string[] | Terms to bias the decoder toward. Max 200, each ≤ 64 chars; blank terms, commas, newlines and control characters are rejected, duplicates collapsed case-insensitively. Biasing applies even when verbatim is on |
| replacements | object[] | Ordered { pattern, replacement } pairs — case-insensitive whole-phrase literals, not regex. Max 100; pattern ≤ 100 chars, single-line and non-blank, replacement ≤ 500 (newlines allowed, other control characters rejected) |
| remove_disfluencies | boolean | Strip filler ("um", "uh"). English only — the filler list is English ("er" is an ordinary German word), so it is a no-op on other languages. Default false |
| remove_hedges | boolean | Also strip hedges ("like", "you know"). Only while remove_disfluencies is on, and English only for the same reason |
| verbatim | boolean | Off-switch for TEXT CLEANUP: keeps the stored config but stops applying replacements and disfluency removal. Vocabulary still biases the decoder — verbatim is about cleanup, not recognition accuracy |
curl -X PUT https://waven.ai/api/v1/me/dictation-profile \
-H "Authorization: Bearer wvn_xxxx_..." \
-H "Content-Type: application/json" \
-d '{
"vocabulary": ["Kokoro", "Parakeet"],
"replacements": [{"pattern": "new para", "replacement": "\n\n"}],
"remove_disfluencies": true,
"remove_hedges": false,
"verbatim": false
}'
# Response (200) — the stored profile, plus when it was written.
# updated_at is UTC with no offset suffix (never "Z"), or null if the
# profile has never been saved:
{
"vocabulary": ["Kokoro", "Parakeet"],
"replacements": [{"pattern": "new para", "replacement": "\n\n"}],
"remove_disfluencies": true,
"remove_hedges": false,
"verbatim": false,
"updated_at": "2026-08-24T10:11:12.345678"
}Over a limit — or a blank term, a multi-line pattern, a stray control character — is a 422, never a silent truncation. The PUT is a write, so it is consent-gated like the other write routes: an account that hasn't accepted the current Terms and Privacy Policy gets a 428 with the missing items listed. Both verbs are account routes, so the marketing demo token is refused with a 403— use an API key or a Clerk JWT. POST /api/v1/stt/transcribe and POST /api/v1/stt/transcribe/jobs apply this profile by default; send apply_dictation_profile=false for raw output. The diarize route never applies it — a speaker-labelled transcript is mostly other people's speech. The WebSocket streaming route doesn't apply it server-side at all: our own clients run the replacement/disfluency pass locally on the final, so a third-party streaming client gets raw text and should apply the profile itself.
| 400 | Bad request (conflicting or unusable parameters) |
| 401 | Missing, invalid, expired, or revoked credentials (body carries error.code=api_key_expired or api_key_revoked when the key itself is the reason) |
| 402 | Quota exhausted (body carries error.code=quota_exhausted; never Retry-After) |
| 404 | No such resource, or not yours |
| 413 | Request body over 200 MB |
| 422 | Validation error (bad parameters) |
| 429 | Rate limited — see Retry-After and the RateLimit-* headers |
| 503 | Model service unavailable |
| GET /api/v1/account/export | Download all your data as JSON |
| POST /api/v1/account/delete | Request account deletion (30-day grace period) |
| POST /api/v1/account/cancel-deletion | Cancel pending deletion |
Routes not covered here — billing, webhooks, the voice gallery, account data — are all in the API reference, generated from the backend's OpenAPI spec.