waven.ai
REFERENCE

API documentation

REST endpoints, authentication, and code examples for the waven.ai API. Every request goes to https://waven.ai.

OPENAPI

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.

Open the API reference
AUTHauthentication

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.

POST/api/v1/generate

Generate speech from text. Returns audio metadata and a download URL.

ParamTypeDescription
text *stringText to synthesize (max 5,000 chars)
model *stringModel name (omnivoice, kokoro)
languagestringLanguage code (default: "Auto")
speakerstringSpeaker ID (model-dependent)
speedfloatSpeed multiplier (0.25 – 4.0)
ref_idstringReference audio ID from /upload-ref (voice cloning)
gallery_voice_idstringSaved gallery voice ID (alternative to ref_id)
formatstringwav, 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.

POST/api/v1/generate-long

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.

GET/api/v1/models

List available models with their status, default voice, and languages. Stock voices live at /api/v1/voices below.

GET/api/v1/voices

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/api/v1/user

Get current user profile, tier, credits, subscription status, and usage stats.

GET/api/v1/user/usage/timeseries

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
    }
  ]
}
GET/api/v1/user/usage/by-key

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
    }
  ]
}
CRUD/api/v1/keys/

Manage API keys for programmatic access.

GET /api/v1/keys/List your API keys (paginated: limit, offset)
POST /api/v1/keys/?name=my-keyCreate 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).

INFOscopes

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:generatePOST /generate and /generate-long, plus the TTS model list and health.
tts:streamThe TTS streaming WebSocket.
stt:transcribeSubmit and cancel transcriptions, plus the STT model list and health.
stt:streamThe STT streaming WebSocket.
stt:diarizeThe diarize route, and diarize=true on a submitted job. Needed alongside stt:transcribe.
jobs:readPoll async jobs and download finished transcripts. Read-only: it never submits.
voices:readList and download your saved voices and voice clones.
voices:writeUpload reference audio, save, rename, replace and delete voices.
account:readProfile, usage, referral, generation history and the dictation profile.
webhooks:manageFull 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.

INFOsandbox keys

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/mcp
  • POST /api/v1/generate
  • POST /api/v1/generate-long
  • POST /api/v1/stt/transcribe
  • POST /api/v1/stt/transcribe/diarize
  • POST /api/v1/stt/transcribe/jobs
  • WS /api/v1/tts/stream
  • WS /api/v1/stt/transcribe/stream
  • POST /api/v1/audio/transcriptions
  • POST /api/v1/audio/speech
  • POST /v1/audio/transcriptions
  • POST /v1/audio/speech
  • GET /api/v1/generate-long/jobs/{job_id}
  • GET /api/v1/stt/transcribe/jobs
  • GET /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/user
  • GET /api/v1/user/usage
  • GET /api/v1/me/dictation-profile
  • GET /api/v1/models
  • GET /api/v1/tts/health
  • GET /api/v1/stt/health
  • GET /api/v1/voices
  • GET /api/v1/consent
  • GET /api/v1/consent/
  • GET /api/v1/consent/onboarding/status
  • POST /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"
POST/api/v1/upload-ref

Upload reference audio for voice cloning. Requires consent attestation.

ParamTypeDescription
reference_audio *fileAudio file (WAV, FLAC, OGG, MP3, M4A; max 10 MB)
reference_textstringTranscript of the reference audio
consent_attested *booleanMust 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.

INFOrate limits
TierRequests/minConcurrent TTSConcurrent STTAPI keysTTS quotaSTT quota
Free1011160 min90 min
Personal20112300 min900 min
Pro603253,000 min6,000 min
Studio12054107,200 min18,000 min
Enterprise30010850CustomCustom

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.

SPEECH-TO-TEXT

STT API

POST/api/v1/stt/transcribe

Transcribe an audio file. Supports auto model selection with fallback chain.

ParamTypeDescription
file *fileAudio file (WAV, MP3, FLAC, OGG, M4A)
modelstringModel: auto, parakeet, voxtral (default: auto)
languagestringLanguage code (optional, auto-detect if omitted)
correctbooleanPost-process with grammar correction LLM
hotwordsstringComma-separated terms to bias the decoder toward
redact_piibooleanMask personal data in the transcript
redact_entitiesstringComma-separated entity types to mask
granularitystringsegment (default) or word — also return per-word timestamps under words
formatstringjson (default), txt, srt or vtt — captions/text as a file download
summarybooleanReturn a summary in audio_intelligence
sentimentbooleanReturn per-segment sentiment
topicsbooleanReturn detected topics
entitiesbooleanReturn extracted named entities
translate_tostringTranslate 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.

POST/api/v1/stt/transcribe/diarize

Transcribe with speaker diarization. Runs ASR + Pyannote in parallel, then merges speaker labels.

ParamTypeDescription
file *fileAudio file
modelstringASR model (default: auto → parakeet)
num_speakersintExact speaker count, 1–50 (overrides min/max)
min_speakersintMinimum speaker count, 1–50
max_speakersintMaximum speaker count, 1–50
hotwordsstringComma-separated terms to bias the decoder toward
redact_piibooleanMask personal data in the transcript
formatstringjson (default), txt, srt or vtt — cues carry SPEAKER_XX: prefixes
summarybooleanReturn 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.

WS/api/v1/stt/transcribe/stream

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.

POST/api/v1/stt/transcribe/jobs

Submit audio for queued batch transcription. Returns a job ID immediately. Poll with GET.

ParamTypeDescription
file *fileAudio file
modelstringASR model (default: auto)
languagestringLanguage code
correctbooleanGrammar correction
hotwordsstringComma-separated terms to bias the decoder toward
redact_piibooleanMask personal data in the transcript
granularitystringsegment (default) or word — the finished result then carries words
summarybooleanReturn a summary in audio_intelligence
sentimentbooleanReturn sentiment in audio_intelligence
topicsbooleanReturn topics in audio_intelligence
entitiesbooleanReturn named entities in audio_intelligence
translate_tostringTranslate the audio into an enabled target language (WAV/MP3, or server-decodable audio; 4 min max; plain transcription only)
diarizebooleanQueue a speaker-diarization job instead (result arrives under diarization)
num_speakersintegerExact speaker count hint, 1-50 (diarize only)
min_speakersintegerLower bound on speakers, 1-50 (diarize only)
max_speakersintegerUpper 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.

GET/api/v1/stt/transcribe/jobs

List your transcription jobs, newest first. Paginated: limit (1–100, default 20), offset.

GET/api/v1/stt/transcribe/jobs/{job_id}

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.

GET/api/v1/stt/transcribe/jobs/{job_id}/transcript.srt

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.
DELETE/api/v1/stt/transcribe/jobs/{job_id}

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
GET/api/v1/stt/models

List available STT models with capabilities and supported languages.

SDKPython · TypeScript · CLI

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/sdk
import { 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 voices

The 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.

POST/api/v1/mcp

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"
}
ToolRequired scopeDescription
transcribe_audiostt:transcribeTranscribe inline audio; supports language, correction, PII redaction and hotwords.
synthesize_speechtts:generateGenerate speech with an optional stock voice; receive audio or a download URL.
list_voicesvoices:readRead the stock voice catalogue and preview URLs.
get_jobjobs:readRead the status and result of your transcription job.
analyze_audiostt:transcribeRequest 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.

POST/v1/audio/transcriptions · /v1/audio/speech

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.

ParamEndpointVerdictNotes
filetranscriptionsacceptedThe audio to transcribe.
modeltranscriptionsacceptedauto | parakeet | voxtral. OpenAI names (whisper-1) are rejected unless an operator opts in to aliasing.
languagetranscriptionsacceptedISO-639-1 hint. Routing drops English-only engines when a non-English hint is present.
prompttranscriptionsapproximatedMapped 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_formattranscriptionsacceptedjson | text | srt | vtt | verbose_json. srt/vtt/text are served as text/plain, as OpenAI serves them.
timestamp_granularities[]transcriptionsacceptedsegment (default) or word. Also accepts timestamp_granularities without brackets. word requires response_format=verbose_json and an engine with alignments — 400 on voxtral.
temperaturetranscriptionsrejectedDecoding is deterministic. Omit it, or send 0.
streamtranscriptionsrejected400 for true; false or omitted is accepted. Use the WebSocket route for realtime.
include[]transcriptionsrejected400 for nonempty values, also for the unbracketed include spelling. Selects Whisper-internal logprobs this API has no equivalent for.
redact_piitranscriptionsrejectedUnavailable on the shim. Use /api/v1/stt/transcribe with redact_pii=true for PII redaction.
(unknown field)transcriptionsrejected400 naming the unknown multipart fields and listing all accepted fields. Only the names in the multipart field list are recognised.
modelspeechacceptedauto | kokoro | omnivoice.
inputspeechacceptedText to synthesize, up to the standard 5,000-character cap (above OpenAI's 4,096).
voicespeechacceptedKokoro: 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_formatspeechacceptedwav | mp3 | ogg. opus/aac/flac/pcm are rejected, not substituted.
speedspeechaccepted0.25–4.0 when given. Omitted means the engine default; SSML prosody is honoured. An explicit speed overrides SSML.
instructionsspeechacceptedMapped to OmniVoice style tags (fixed allowlist). A verified no-op on kokoro.
stream_formatspeechrejected400. This endpoint returns the complete clip in one response.
(unknown field)speechrejected400 naming the field. The JSON body is a closed set.
ParamTypeDescription
multipart fields (STT)formfile, model, language, prompt, response_format, temperature, timestamp_granularities[], timestamp_granularities, stream, include[], include
model (STT)stringauto | parakeet | voxtral
model (TTS)stringauto | kokoro | omnivoice
response_format (STT)stringjson | text | srt | vtt | verbose_json
response_format (TTS)stringwav | 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.

CRUD/api/v1/me/dictation-profile

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-profileRead the profile. Never 404s — an account that has never saved one gets the defaults with updated_at: null.
PUT /api/v1/me/dictation-profileReplace it. Full-document, last-write-wins — there is no PATCH, so always send every field.
ParamTypeDescription
vocabularystring[]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
replacementsobject[]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_disfluenciesbooleanStrip 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_hedgesbooleanAlso strip hedges ("like", "you know"). Only while remove_disfluencies is on, and English only for the same reason
verbatimbooleanOff-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.

INFOerror codes
400Bad request (conflicting or unusable parameters)
401Missing, invalid, expired, or revoked credentials (body carries error.code=api_key_expired or api_key_revoked when the key itself is the reason)
402Quota exhausted (body carries error.code=quota_exhausted; never Retry-After)
404No such resource, or not yours
413Request body over 200 MB
422Validation error (bad parameters)
429Rate limited — see Retry-After and the RateLimit-* headers
503Model service unavailable
GDPRdata rights
GET /api/v1/account/exportDownload all your data as JSON
POST /api/v1/account/deleteRequest account deletion (30-day grace period)
POST /api/v1/account/cancel-deletionCancel 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.