Browse API documentation
Apply for API access

POST /v1/speech

Create Persian speech

Convert Persian text to audio with a Basic or Premium voice. The synchronous endpoint returns a signed download URL. For Basic live audio, create a job and follow its event stream.

POSThttps://api.vowelmarks.com/v1/speech

Updated October 27, 2026 · API version 1

Tier availability: asynchronous Basic jobs, Basic MP3 event streams, and completed-audio HEAD/range requests are available in production. Asynchronous Premium jobs and Premium PCM event streams remain a development/staging preview and are disabled in production. Existing synchronous Basic and Premium integrations continue to use POST /v1/speech.

Delivery modes

IntegrationProduction behavior
Basic or Premium: POST /v1/speechWait for the finished recording, receive JSON, then download from its signed audio_url.
Basic: POST /v1/speech/jobsReceive a job ID immediately after durable acceptance. Follow its events or poll for completion.
Basic: job /eventsFresh Kian and Leyla recordings can send MP3 audio before completion. Legacy voices and cache hits may send no preview audio.
Premium: asynchronous jobs and live audioNot enabled in production. Use synchronous Premium for an integration launching today.

Website Speech Studio and the Developer API are separate contracts. Enabling streaming on the website does not enable Premium API jobs. This API accepts the fields in its published schema, not the website's dialogue, inline-cue, or designed-voice objects.

Development and staging: Premium jobs

A Premium development/staging preview must be explicitly enabled in that environment. When provider streaming is enabled, it can emit raw PCM audio; when provider streaming is disabled, it sends status updates and publishes the completed WAV instead. Confirm that the preview is enabled before integrating against it.

Basic speech

Basic accepts kian, leyla, ava, mana, or amir. Kian and Leyla are the featured voices; Ava, Mana, and Amir remain available as self-hosted alternatives. pace defaults to 1 and accepts 0.75–1.25. Premium-only fields are rejected rather than ignored.

ParameterTypeDescription
textstring · requiredPersian text, up to 6,000 Unicode code points. The complete JSON body must fit within 32 KiB.
tierstring · requiredSet to basic.
voicestring · requiredOne of the five Basic voice IDs listed above.
pacenumber · optionalSpeaking rate from 0.75 to 1.25. Default: 1. This changes synthesis, not the speed of a finished file.
{
  "text": "امروز هوا خوب است.",
  "tier": "basic",
  "voice": "kian",
  "pace": 1
}

Kian and Leyla use Edge speech. If the selected voice is unavailable, the request fails instead of substituting Ava. Choose Ava, Mana, or Amir explicitly if you want one of the older Basic voices.

Premium speech

New Premium API requests currently use Google's gemini-3.8-flash-tts model. Basic uses the separate voices described above. The openapi: 3.1.0 header in our downloadable schema identifies the OpenAPI specification format; it is not a Gemini model version. The API does not accept a client-selected model parameter.

Premium accepts a voice from the published voice registry plus optional pace, style, expressiveness, regional_mode, and director_notes. Defaults are pace 1, style neutral, expressiveness natural, regional mode standard, and no director notes.

regional_mode accepts only standard and can be omitted.

ParameterTypeDescription
textstring · requiredPersian text. The same 6,000-code-point and 32 KiB body limits apply.
tierstring · requiredSet to premium.
voicestring · requiredA voice ID from the published registry, such as Kore. Use the API ID, not a website display name.
pacenumber · optional0.75–1.25. Default: 1.
stylestring · optionalneutral, warm, news, conversational, instructional, or storytelling. Default: neutral.
expressivenessstring · optionalcalm, natural, or lively. Default: natural.
regional_modestring · optionalstandard only. Default: standard.
director_notesstring or null · optionalA nonempty string of at most 600 UTF-16 code units (JavaScript string.length), or null. Describe delivery, such as warmth, articulation, or pauses; keep the words to speak in text. Default: null. Emoji can count as two units; ordinary Persian letters count as one.
{
  "text": "امروز هوا خوب است.",
  "tier": "premium",
  "voice": "Kore",
  "style": "warm",
  "expressiveness": "natural",
  "regional_mode": "standard",
  "director_notes": "Gentle, clear, and unhurried."
}

Premium provider capacity can temporarily return retryable 503 service_unavailable. Honor the response's Retry-After value, reuse the original idempotency key and exact body, and retry sequentially. Stop after three attempts and queue the request for later rather than running an unbounded retry loop.

Vocabulary and separate clips

Each production speech request accepts one text string and returns one recording. Arrays of words, an items parameter, automatic silence splitting, and a multi-file batch route are not available. Sending several sentences in one string produces one continuous recording; punctuation and delivery notes do not guarantee exact pause durations or independent takes.

For separate clips today, send each distinct item sequentially with its own stable idempotency key, download the result, and retain your own item-to-file mapping. Retry an uncertain item with its original key and exact body. An idempotent replay avoids another generation and charge; submitting the same text with a new key is a new metered request even if the audio comes from cache.

Your organization's requests-per-minute limit, its remaining audio-second allowance, and shared upstream provider capacity are different constraints. Many very short Premium clips can exhaust provider daily requests while using little audio time. Honor Retry-After on retryable failures; more parallel requests do not increase the provider's daily capacity. Check your own plan and remaining allowance with GET /v1/usage.

Current duration_seconds measures the complete recording, including silence. usage.seconds reports settled usage; no silence-exclusion or per-item billing option is implemented. The website's monthly final-recording grace is a separate contract and does not extend the Developer API's hard-capped Premium allowance.

Response and audio download

A successful response includes audio_url, audio_expires_at, mime_type, duration_seconds, the chosen tier and voice, and resolved delivery settings. The MIME type is audio/wav or audio/mpeg; use it when storing or serving the downloaded file. The signed URL expires after one hour. Replaying an idempotent request can issue a fresh URL to the same cached audio without a second usage charge.

The POST is synchronous: it waits for complete generation, storage, and usage settlement, then returns JSON rather than audio bytes or base64. Make a separate GET request to audio_url to download the completed file. That GET has a streamed HTTP response body, but it is not live synthesis streaming and does not provide earlier playback while speech is generated.

Basic can return WAV or MP3: short clips stay lossless, while longer clips may be compressed to reduce download size. Premium returns WAV. Always check mime_type rather than assuming the file format from the voice.

For example, after saving the successful JSON response as speech.json, download it with curl --fail --show-error --output speech.wav "$(jq -r '.audio_url' speech.json)". Use speech.mp3 instead when mime_type is audio/mpeg. The signed URL authorizes this download; do not send your API key to it. Save the file on your server if you need it after the URL expires.

The following is an audio-fields excerpt; the complete response also contains request_id, engine_version, original, and usage.

{
  "audio_url": "https://api.vowelmarks.com/v1/developer/audio?…",
  "audio_expires_at": "2026-08-10T20:00:00.000Z",
  "mime_type": "audio/wav",
  "duration_seconds": 3.42,
  "tier": "premium",
  "voice": "Kore",
  "delivery": {
    "pace": 1,
    "style": "warm",
    "expressiveness": "natural",
    "regional_mode": "standard",
    "director_notes": "Gentle, clear, and unhurried."
  }
}

Asynchronous Basic speech jobs

All job routes require a server-side Bearer key with the speech scope. In production, POST /v1/speech/jobs accepts the same Basic JSON object as POST /v1/speech, but returns 202 Accepted after the VowelMarks Workflow durably queues the job instead of holding the creation request open for synthesis. Idempotency-Key is required. The same key and exact body return the existing job and never start or charge a second generation.

The request schema also documents Premium for development and staging. In production, a valid Premium job request returns non-retryable 503 premium_speech_jobs_unavailable before allowance reservation, storage, or Workflow start. Use synchronous POST /v1/speech for Premium, or submit a Basic job.

curl --request POST https://api.vowelmarks.com/v1/speech/jobs \
  -H "Authorization: Bearer $VOWELMARKS_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: narration-section-42" \
  --data '{"text":"امروز هوا خوب است.","tier":"basic","voice":"kian","pace":1}'

The creation response contains the job ID, request ID, current status, tier, preview format, timestamps, and absolute polling, event-stream, and cancellation URLs. Status is one of queued, running, succeeded, failed, or cancelled. Production Basic jobs use stream_format: "mp3". Premium's published preview format is pcm_s16le. In an enabled Premium preview with provider streaming disabled, sections run in buffered mode: the job emits status updates without audio events, then publishes one completed WAV file.

{
  "id": "00000000-0000-4000-8000-000000000042",
  "request_id": "95b…",
  "status": "queued",
  "tier": "basic",
  "stream_format": "mp3",
  "created_at": "2026-09-15T23:00:00.000Z",
  "started_at": null,
  "finished_at": null,
  "status_url": "https://api.vowelmarks.com/v1/speech/jobs/00000000-0000-4000-8000-000000000042",
  "stream_url": "https://api.vowelmarks.com/v1/speech/jobs/00000000-0000-4000-8000-000000000042/events",
  "cancel_url": "https://api.vowelmarks.com/v1/speech/jobs/00000000-0000-4000-8000-000000000042/cancel"
}

Poll GET /v1/speech/jobs/:id when streaming is unnecessary or after a stream interruption. It returns the same job fields. A succeeded job also includes result, usage, audio_url, and audio_expires_at. A failed job includes the normal structured error object. Reading a succeeded job again within its 24-hour job lifetime renews the signed URL without regenerating or charging again.

Follow ordered Basic synthesis events

GET /v1/speech/jobs/:id/events is an authenticated Server-Sent Events response. It sends ordered status, audio, reset, complete, and error events plus heartbeat comments while the job is active. Status data is {"job_id":"…","status":"running"}. A production Basic audio event is {"job_id":"…","format":"mp3","audio_base64":"…"}. The Premium schema reserves pcm_s16le for signed 16-bit little-endian PCM at 24 kHz mono when preview streaming is enabled. A buffered Premium preview emits no audio events before completion. Treat status-only SSE as normal and poll the job until it reaches a terminal state and exposes the finished WAV.

Basic live preview is currently available only when fresh Kian or Leyla synthesis emits audio. Legacy Ava, Mana, or Amir voices and cache hits can reach complete without any audio events. Clients must tolerate zero preview chunks and always await complete or poll the final job.

curl --no-buffer \
  -H "Authorization: Bearer $VOWELMARKS_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Last-Event-ID: 17" \
  "https://api.vowelmarks.com/v1/speech/jobs/$JOB_ID/events"

A reset event means previously streamed preview audio must be discarded before accepting later chunks. Preview bytes are not the durable completed file and do not settle usage. Use the terminal complete event or a succeeded polling response for the final artifact and accounting result.

A complete event contains the succeeded job object and event: "complete". An error event contains the job ID, failed status, and structured error. Every replayable event has a monotonic positive integer SSE ID. Reconnect with Last-Event-ID, or pass the same value as ?after=<id> when your HTTP client cannot set that header; the query value wins if both are present. A stream disconnect does not cancel the job. End-of-file is not success: if the connection closes before complete or error, reconnect or poll the job until it reaches a terminal status.

Browser playback depends on the preview format. Basic MP3 chunks can be appended only when the browser supports MP3 through Media Source Extensions. Premium pcm_s16le preview data, when enabled in development or staging, is raw PCM rather than MP3 or WAV. A buffered Premium preview has no live preview audio; keep polling the accepted job for its completed WAV. Playback or event-stream failure does not cancel generation.

Developer keys remain server-side secrets. Browser EventSource cannot safely carry the Bearer key; open and parse this stream from your backend, then forward only the events your client needs.

Recover, cancel, and retain jobs

POST /v1/speech/jobs/:id/cancel requests best-effort cancellation, returns the current job object, and is safe to repeat. If completion wins the race, the job succeeds and the normal generated duration is charged. If cancellation or failure wins before completion, the reservation is released. Closing the client event stream alone never requests cancellation, and the VowelMarks Workflow continues independently.

Development and staging: buffered Premium recovery

For an explicitly enabled development/staging Premium preview in buffered mode, VowelMarks Workflow owns the asynchronous job while provider streaming is disabled. It requests and validates each planned section as a complete buffered response, privately checkpoints completed section audio, and assembles one finished file for the client job. In that mode, submit once, keep the same job ID, and poll rather than expect live audio. Long-form Premium completion and restart recovery remain under validation for the Developer API independently of website Speech acceptance.

A job can run for at most two hours. Job input and replayable event metadata remain available for 24 hours. Successfully completed cache audio follows the normal 30-day sliding cache policy, while each signed download URL lasts one hour. Save the completed file or poll the still-live job for a fresh signed URL before job metadata expires.

Completed audio downloads support HEAD, Content-Length, Content-Type, ETag, Accept-Ranges: bytes, If-None-Match, and one byte range with If-Range. A valid range returns 206 Partial Content; an unsatisfiable range returns 416. The signed URL is the complete authorization; do not add the API key.

Both job creation and synchronous speech retain the same 6,000-code-point and 32 KiB JSON limits.

Development and staging: Premium section planning

The durable Premium path estimates section duration, aims near 150 seconds, and treats an estimated 180 seconds as its section-planning ceiling. Those figures guide planning only; they neither guarantee nor hard-limit the audio duration the provider returns. Asynchronous execution lets one client creation request continue as a Workflow and eventually expose one finished file, but it does not remove provider per-interaction duration, output-size, or quality limits. Section planning and completed-section checkpoints reduce expected interaction size and restart work; they do not guarantee provider completion.

Common creation failures include 400 idempotency_key_required, 402 basic_speech_quota_exhausted, 409 idempotency_conflict, 429 rate_limit_exceeded, and retryable 503 speech_jobs_unavailable or speech_job_start_uncertain. Production Premium job creation returns non-retryable 503 premium_speech_jobs_unavailable with the action “Use the synchronous speech endpoint or submit a Basic speech job.” Authentication and scope failures use 401 or 403. Job reads are organization- and environment-scoped, so an inaccessible job is returned as 404. For an uncertain Basic start, retry the exact body with the original idempotency key. A provider_submission_uncertain job failure is different: VowelMarks cannot prove whether the speech provider accepted the request, so do not start another provider generation automatically.

Long text and mobile integrations

Each request accepts at most 6,000 Unicode code points and the complete serialized JSON body must fit within 32 KiB (32,768 bytes). For longer passages, your server must split the source at sentence or paragraph boundaries, retain the original order, and send a separate request for each section. Count with Array.from(text).length in JavaScript. Spaces, ZWNJ, and combining marks count too; do not split a combining sequence or remove characters when forming sections.

Give each section its own idempotency key, and reuse that exact key and payload only for a retry of that section. Start with sequential requests; limit any later concurrency to your approved capacity and shared rate limit. Completed sections are charged even if a later section fails. Retry only the failed section when its error is retryable.

Download each audio file before its signed URL expires. A failed complete-file GET can be retried with the same signed URL while it remains valid because generation and usage settlement already completed. Play sections in order, or combine compatible audio using an audio tool; concatenating WAV or MP3 file bytes is not a valid general-purpose merge. Voices and pacing may vary slightly between separate generations.

Some speech origins sleep while idle, so the first request can spend tens of seconds waking an origin before audio generation begins. Status updates and heartbeat events during this wait contain no audio. Long synthesis can take several minutes. Prefer asynchronous Basic jobs for long recordings so a client disconnect does not interrupt the job. For a maximum-size synchronous request, allow up to 15 minutes or use shorter sections. If the POST times out, reuse its original idempotency key and exact payload; a new key can start and charge a second generation.

For a mobile or browser app, call VowelMarks from your own backend and deliver the result to the app. Never ship your API key in the client. Treat signed audio URLs as temporary access credentials and keep them out of logs and analytics. Word timings, captions, pronunciation overrides, batch processing, and asynchronous Premium jobs are not part of the current production API contract.

Allowances

  • Basic and Premium seconds are separate from text units.
  • Basic allows one request to finish when at least one second remained at generation start; later requests stop until the allowance resets.
  • Premium reserves a conservative upper bound before generation and is hard-capped.
  • If valid Premium audio exceeds its reservation, VowelMarks discards it, charges nothing, and returns a retryable service error.
  • Speech overage billing is not part of API v1.