# Introduction

> Munsit is the Arabic voice API. One key gets you natural Arabic speech, the most accurate Arabic speech recognition available, and everything in between. Here's the first call.

## Your first call

Copy this, paste your key, run it.

```
curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"voice_id":"...","text":"مرحبا بك في منصت"}' \
  --output speech.wav
```

```
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@meeting.mp3"
```

```
wscat -c "wss://api.munsit.com/api/v1/websocket/speech-to-text" \
  -H "x-api-key: $MUNSIT_API_KEY"

# send audio chunks, read transcripts back
```

```
curl -X POST "https://api.munsit.com/api/v1/voices/clone" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@sample.mp3" \
  -F "name=Layla"
```

Response

```
// audio bytes — save and play
```

Response

```
{
  "statusCode": 200,
  "data": {
    "transcriptionId": "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47",
    "transcription": "اجتماع الفريق يبدأ الساعة العاشرة"
  },
  "message": "Success"
}
```

Response

```
// transcripts arrive as the speaker talks
{ "type": "transcript", "transcription": "اجتماع الفريق يبدأ الساعة العاشرة" }
```

Response

```
{ "voice_id": "cl-layla-8f21", "status": "ready" }
```

Need a key? Create one in the dashboard — free credits on signup, no card. [Get an API key →](https://app.munsit.com/en/api-keys)

## The whole surface

Every endpoint and agent plugin, all behind that one key. Nothing here is a separate signup.

[

### Text to Speech

→](/text-to-speech/get-started)

faseeh · natural Arabic voices

-   [Synthesize /text-to-speech/{model}](/text-to-speech/synthesize)
-   [Stream audio out /websocket/text-to-speech](/text-to-speech/audio-streaming-output)
-   [List voices /voices](/text-to-speech/voices)
-   [Preview a voice /voices/preview](/text-to-speech/voice-preview)
-   [Clone a voice /voices/clone](/text-to-speech/voice-cloning)
-   [Tashkīl — diacritize /tashkil/diacritize](/text-to-speech/tashkil)

[

### Speech to Text

→](/speech-to-text/get-started)

munsit · #1 on Arabic ASR

-   [Transcribe /audio/transcribe](/speech-to-text/transcribe)
-   [Streaming /websocket/speech-to-text](/speech-to-text/streaming)
-   [Speaker diarization /audio/diarization/transcribe](/speech-to-text/diarization)
-   [Diarization + sentiment /diarization/{id}/sentiment-analysis](/speech-to-text/diarization-sentiment)
-   [Minutes of a meeting /minutes-of-meeting/transcribe](/speech-to-text/minutes-of-meetings)

[

### Understanding

→](/understanding/sentiment-analysis)

on top of any transcript

-   [Sentiment analysis /audio/{id}/sentiment-analysis](/understanding/sentiment-analysis)
-   [Keyword extraction /minutes-of-meeting/{id}/keyword-extraction](/understanding/keyword-extraction)
-   [Translation /translation/stream](/understanding/translation)

[

### Audio

→](/audio/voice-isolation)

clean it before you use it

-   [Voice isolation /denoise](/audio/voice-isolation)
-   [Job status /denoise/{id}/progress](/audio/denoise-progress)
-   [List models /models](/text-to-speech/models)

[

### Voice agents

→](/integrations/livekit)

drop-in plugins

-   [LiveKit STT + TTS plugin](/integrations/livekit)
-   [Pipecat plugin](/integrations/pipecat)
-   [VAPI guide](/integrations/vapi)
-   [Ultravox guide](/integrations/ultravox)

[

### Getting started

→](/quickstart)

before you build

-   [Quickstart guide](/quickstart)
-   [Authentication guide](/authentication)
-   [Rate limits guide](/rate-limits)
-   [Errors guide](/errors)
-   [Support guide](/support)

## Pick your path

Four common jobs. Each one is a guide that ends with working code.

[Generate Arabic speech Natural voices across Gulf dialects, plus MSA. faseeh · /text-to-speech](/text-to-speech/get-started) [Transcribe Arabic audio Files or live. 25+ dialects, no dialect parameter needed. munsit · /audio/transcribe](/speech-to-text/get-started) [Stream in real time Two WebSockets — synthesize as text arrives, or transcribe as they speak. wss · /websocket/\*](/text-to-speech/audio-streaming-output) [Build a voice agent Drop-in plugins for LiveKit, Pipecat, VAPI and Ultravox. 5 integrations](/integrations/livekit)

## Running it yourself

If you can't send audio to a cloud, you don't have to.

**On-premises deployment.** Run Munsit entirely inside your own data centre or private environment. [Self hosting →](/deployment/self-hosting)[Data privacy →](/deployment/compliance)

## Build with an AI assistant

These docs are also an MCP server, so your assistant can read them itself instead of you pasting pages into a chat window.

```
claude mcp add --transport http munsit-docs https://docs.munsit.com/api/mcp
```

**Five tools:** search the docs, read a page, list every page, list every endpoint, and pull one endpoint's full schema. No key, no auth, nothing to install. [MCP server →](/mcp)[llms-full.txt →](/llms-full.txt)


---

# Quickstart

> Munsit is a RESTful API you can call from any language or framework — high-quality Arabic voice synthesis across dialects (Fusha, Emirati, Saudi Najdi, Saudi Hijazi and more), streaming audio output, customizable voice parameters, and Arabic speech recognition. Three steps to your first result.

1

## Get an API key

Sign up and generate an API key from the dashboard for your region. You'll send it in the `x-api-key` header.

> Keys are region-specific. Each endpoint requires an API key from its corresponding dashboard — a global key won't authenticate against the UAE endpoint, and vice versa. See Authentication for details.

[Create a Global API key →](https://app.munsit.com/en/api-keys)

2

## Make your first request

Put your key in the `x-api-key` header, point it at an audio file, and send it.

```bash
# 1. your key, from the dashboard
export MUNSIT_API_KEY="eyJhbGciOiJIUzI1NiIs..."

# 2. send audio, get Arabic text
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@meeting.mp3"
```

```python
import os, requests

r = requests.post(
    "https://api.munsit.com/api/v1/audio/transcribe",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    files={"file": open("meeting.mp3", "rb")},
)

print(r.json()["data"]["transcription"])
```

```javascript
import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("meeting.mp3")]), "meeting.mp3");

const res = await fetch(
  "https://api.munsit.com/api/v1/audio/transcribe",
  { method: "POST",
    headers: { "x-api-key": process.env.MUNSIT_API_KEY },
    body: form }
);

const { data } = await res.json();
console.log(data.transcription);
```

```go
package main

import (
	"bytes"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	file, _ := os.Open("meeting.mp3")
	defer file.Close()

	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	part, _ := w.CreateFormFile("file", "meeting.mp3")
	io.Copy(part, file)
	w.Close()

	req, _ := http.NewRequest("POST", "https://api.munsit.com/api/v1/audio/transcribe", &buf)
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", w.FormDataContentType())

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(string(body))
}
```

Response — 200

```
{
  "statusCode": 200,
  "data": {
    "transcriptionId": "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47",
    "transcription": "اجتماع الفريق يبدأ الساعة العاشرة",
    "duration": 4.56
  },
  "message": "Success"
}
```

> Prefer speech out instead of text? Munsit also does text-to-speech: real-time Arabic voice synthesis with multiple dialects, streaming audio output, and stability / speed voice parameters. The Authentication page shows a full text-to-speech request.

## Explore the documentation

Pick what's next.

- [Get an API key](https://app.munsit.com/en/api-keys) — Generate and manage your API keys from the dashboard. — `app.munsit.com`

- [Text to Speech API](/text-to-speech/get-started) — Arabic voice synthesis with streaming audio output. — `POST /text-to-speech`

- [Speech to Text API](/speech-to-text/transcribe) — Upload audio and get Arabic transcription with streaming support. — `POST /audio/transcribe`

- [Minutes of Meeting API](/speech-to-text/minutes-of-meetings) — Transcribe meeting recordings with structured output. — `POST /minutes-of-meeting`

> Stuck? Every error the API returns is listed on Errors, and a human answers on Support.


---

# Authentication

> One API key. Create a key in the dashboard and you're done — no OAuth dance, no token exchange.

## Send your key as a header

Send your API key in the `x-api-key` HTTP header.

```
x-api-key: YOUR_API_KEY
```

> No key yet? Generate one from the dashboard at app.munsit.com (or ae.app.munsit.com for the UAE). Keys work on every endpoint immediately.

## Make an authenticated request

Paste the command below into your terminal to run your first API request. Replace `$MUNSIT_API_KEY` with your secret API key.

```bash
curl 'https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview' \
  -H 'Content-Type: application/json' \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "streaming": true,
    "speed": 1
  }'
```

```python
import requests

headers = {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
}

response = requests.post(
    'https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview',
    headers=headers,
    json={
        'voice_id': 'ar-najdi-male-2',
        'text': 'مرحبا بك في فصيح',
        'stability': 0.5,
        'streaming': True,
        'speed': 1
    }
)
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    voice_id: 'ar-najdi-male-2',
    text: 'مرحبا بك في فصيح',
    stability: 0.5,
    streaming: true,
    speed: 1
  })
});
```

```go
package main

import (
	"bytes"
	"net/http"
	"os"
)

func main() {
	payload := []byte(`{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "streaming": true,
    "speed": 1
  }`)

	req, _ := http.NewRequest("POST", "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview", bytes.NewReader(payload))
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
}
```

Response

```
// audio stream — save and play
```

## WebSocket authentication

REST requests always use the `x-api-key` header. WebSocket endpoints accept it too, plus a query-parameter form for browsers, which cannot set headers on a WebSocket connection.

| Endpoint | Header | Query parameter |
| --- | --- | --- |
| [`WS /api/v1/listen`](/speech-to-text/streaming) — speech to text | `x-api-key` | `api_key` (or `x-api-key`) |
| [`WS /websocket/text-to-speech`](/reference/websocket) | `x-api-key` or `Authorization: Bearer` | `x-api-key` — also accepted in the `initConnection` message |

```
const ws = new WebSocket("wss://api.munsit.com/api/v1/listen?api_key=YOUR_API_KEY");
```

> Query-parameter auth puts the key in the URL. Keep connection URLs out of your logs and analytics. If authentication fails on /api/v1/listen, the connection closes with code 1008 after an Error event.

## Scope your keys

A key isn't all-or-nothing. Each key can be configured with restrictions and quotas from the dashboard.

| Control | What it does | Use it when |
| --- | --- | --- |
| **Endpoint restrictions** | Control which API endpoints each key can access — a transcription-only key can't call voice cloning. | Per-service keys, or a key you hand to a contractor. |
| **Usage limits** | Set custom quotas to manage and monitor your API consumption. | Staging keys, per-customer keys, anything you don't want running away. |

## Pick your region

The examples above use the global endpoint (`api.munsit.com`). If you're in the UAE, use the regional endpoint (`ae.api.faseeh.ai`) with an API key from the UAE dashboard. See [All endpoints](/reference/endpoints) for more details.

| Region | Base URL | Dashboard |
| --- | --- | --- |
| **Global** · default | https://api.munsit.com/api/v1 | [app.munsit.com](https://app.munsit.com/) |
| **UAE** · data residency | https://ae.api.faseeh.ai/api/v1 | [ae.app.munsit.com](https://ae.app.munsit.com/) |

> API keys are region-specific. Each endpoint requires a key from its corresponding dashboard — a key issued by the global dashboard won't authenticate against the UAE endpoint. Deploying to the UAE? Generate a separate key at ae.app.munsit.com.

## Keep it secret

Your key is a bearer credential — anyone holding it can spend your credits. Keep it secure and private.

|  | Rule | Why |
| --- | --- | --- |
| ✕ | **Never commit keys to version control** | Public repo history is scraped continuously. A leaked key is spent within minutes. |
| ✕ | **Never share a key publicly** | Anyone with the key can call the API as you and consume your quota. |
| ✕ | **Never include a key in client-side applications** | Anything shipped to the browser or a mobile app is readable. Proxy through your own backend. |
| ✓ | **Use environment variables, scope, and rotate** | Pair with the endpoint restrictions and usage limits above to bound the blast radius. |

## When it fails

Authentication and access failures come back as standard HTTP status codes with a numeric `errorCode`. The full list lives on [Errors](/errors).

| Status | Error code | Meaning | Fix |
| --- | --- | --- | --- |
| 401 | 40101 | Missing or invalid key | Check the header name is exactly `x-api-key`, and that the key hasn't expired or been revoked. In `curl`, wrap the header in **double** quotes — `'…$MUNSIT_API_KEY'` sends the literal text instead of your key. |
| 403 | 40301 | Access denied | The key or account can't reach this model or endpoint. Contact [support](/support) to request access. |
| 429 | 42901 | Concurrency limit exceeded | Wait for current requests to complete or upgrade your plan — see [Rate limits](/rate-limits). |


---

# Rate limits

> The Munsit API uses a credit-based system with concurrent request limits to ensure fair usage and optimal performance for all users. Here's how the limits work, what happens when you hit one, and how to handle it.

## Concurrent requests

For streaming text-to-speech requests, Munsit enforces concurrent request limits based on your subscription plan. This ensures stable performance and prevents system overload.

| Plan | Concurrent request limit |
| --- | --- |
| **Free / no plan** | 1 concurrent request |
| **Basic** | 2 concurrent requests |
| **Starter** | 5 concurrent requests |
| **Growth** | 10 concurrent requests |
| **Scale** | 20 concurrent requests |
| **Enterprise** | Unlimited concurrent requests |

## Live streaming sessions

Streaming sessions on [`WS /api/v1/listen`](/speech-to-text/streaming) have their own limits, separate from the concurrent-request limits above.

| Limit | Value |
| --- | --- |
| **Concurrent sessions** | 5 simultaneous streaming sessions per API key by default, raised on request. Exceeding it closes the new connection with code `1008`. |
| **Idle timeout** | 12 seconds with neither audio nor a `KeepAlive` message closes the session (code `1011`). |
| **Pacing** | Audio may be buffered at most 60 seconds ahead of real time (code `4008`). |
| **Session length** | Unlimited while the connection stays active; unbroken speech is force-segmented about every 60 seconds so results keep flowing. |

> Billing. Seconds of audio received × number of channels, charged from your wallet in 60-second cycles. Connecting requires roughly 60 seconds of wallet runway; running out mid-session closes the connection with code 1008. The closing Metadata event reports the session total as audio_seconds_billed.

## What happens when you exceed a limit

When you exceed your concurrent request limit, the API responds with HTTP `429` and error code `42901` (**ConcurrencyLimitError**). Either wait for existing requests to complete, or upgrade your plan to increase your limit.

```
{
  "errorCode": 42901,
  "errorMessage": "Concurrency limit exceeded. Maximum 5 concurrent requests allowed. Current: 6. Please upgrade your plan https://app.munsit.com/en/subscription or contact support@munsit.com for more information."
}
```

> The full error format — and every other error the API can return — is documented on Errors.

## Best practices

Three habits keep you clear of 429s in production.

| Practice | What to do |
| --- | --- |
| **Monitor your usage** | Check your credit balance and concurrent request limits regularly from the dashboard. |
| **Plan ahead** | Consider your usage patterns when selecting a subscription plan. |
| **Handle rate limits** | Implement retry logic with exponential backoff for `429` errors, and queue requests in your application rather than firing them all at once. |

## Subscription plans

For detailed information about subscription plans, credit limits, concurrent request limits, and pricing, visit the subscription page in the dashboard.

- [View subscription plans →](https://app.munsit.com/en/subscription) — Credit limits, concurrency limits, and pricing per plan. — `app.munsit.com/en/subscription`

- [Errors →](/errors) — Every status code and error code the API returns, with fixes. — `42901 · ConcurrencyLimitError`


---

# Errors

> The Munsit API uses standard HTTP status codes and custom numeric error codes to indicate what went wrong. All errors come back in one consistent JSON format, so a single handler covers every endpoint.

## Error response format

Every error response follows this structure. The HTTP status code in the response header corresponds to the error type, while `errorCode` provides a specific numeric code for programmatic error handling.

```
{
  "errorCode": 40001,
  "errorMessage": "Error description"
}
```

## Error code reference

Every code the API returns, in one table.

| Error code | HTTP status | Error type | Description |
| --- | --- | --- | --- |
| 40001 | 400 | **ValidationError** | Request validation failed |
| 40101 | 401 | **AuthenticationError** | Authentication failed or invalid API key |
| 40201 | 402 | **InsufficientBalanceError** | Insufficient account balance |
| 40301 | 403 | **ModelAccessError** | Model access denied |
| 40401 | 404 | **NotFoundError** | Resource not found |
| 42901 | 429 | **ConcurrencyLimitError** | Concurrent request limit exceeded |
| 50001 | 500/502/503/504 | **InternalError** | Internal server or external service error |

## Error types

Each type carries an `errorCode` (number) and a human-readable `errorMessage` (string). Switch tabs for a real example body of each one.

```
{
  "errorCode": 40001,
  "errorMessage": "Text must have at least 3 words and 10 characters"
}
```

```
{
  "errorCode": 40101,
  "errorMessage": "Invalid API key"
}
```

```
{
  "errorCode": 40201,
  "errorMessage": "Insufficient wallet balance. Required: $0.50, Available: $0.25"
}
```

```
{
  "errorCode": 40301,
  "errorMessage": "This model is not enabled for your account. Please contact support@munsit.com to get access."
}
```

```
{
  "errorCode": 40401,
  "errorMessage": "Model not found: faseeh-v2-preview"
}
```

```
{
  "errorCode": 42901,
  "errorMessage": "Concurrency limit exceeded. Maximum 5 concurrent requests allowed. Current: 6. Please upgrade your plan https://app.munsit.com/en/subscription or contact support@munsit.com for more information."
}
```

```
{
  "errorCode": 50001,
  "errorMessage": "Internal server error"
}
```

| Type | Common scenarios |
| --- | --- |
| **ValidationError**  
400 · 40001 | Empty or missing required fields · text too short (less than 3 words or 10 characters) · parameter values out of range · invalid message types (WebSocket) · missing voice embeddings · payload too large. |
| **AuthenticationError**  
401 · 40101 | Missing `x-api-key` header · invalid API key format · expired API key · revoked or deleted API key · invalid JWT signature. |
| **InsufficientBalanceError**  
402 · 40201 | Account balance is insufficient to complete the request. The message includes the exact amount required and the current available balance — recharge to continue. |
| **ModelAccessError**  
403 · 40301 | The model isn't enabled for your account. Some models require special access — contact [support](/support) to request it. |
| **NotFoundError**  
404 · 40401 | Model not found · voice not found · resource endpoint not found. Some cases return `50001` instead. |
| **ConcurrencyLimitError**  
429 · 42901 | Concurrent request limit for your plan exceeded — Free/no plan 1, Basic 2, Starter 5, Growth 10, Scale 20. See [Rate limits](/rate-limits). |
| **InternalError**  
5xx · 50001 | Internal server errors · external API failures · database connection issues · file storage failures · request timeouts · connection errors. Returned with status 500, 502, 503, or 504. |

## Handling errors

Check the HTTP status code in the response header and the `errorCode` field in the body, then take the matching action.

| Status | Action | Common fixes |
| --- | --- | --- |
| 400 | **Fix the request** | Provide all required fields · meet the text minimum (3 words, 10 characters) · keep parameter values in range · reduce payload size if exceeding limits. |
| 401 | **Verify your API key** | Include the `x-api-key` header · check the key hasn't expired · verify the key format · generate a new key if the current one was revoked. |
| 402 | **Recharge your balance** | Check your balance · add funds via the dashboard · enable auto-topup · verify the cost of the operation before making the request. |
| 403 | **Request model access** | Contact support@munsit.com · verify your plan includes the model · check the model ID is correct. |
| 404 | **Verify the resource exists** | Check the model ID, voice ID, or resource ID · verify the resource hasn't been deleted · make sure you're using the correct endpoint. |
| 429 | **Reduce concurrency or upgrade** | Wait for current requests to complete · reduce simultaneous calls · upgrade your plan · implement request queuing in your application. |
| 5xx | **Retry, then contact support** | Retry after a short delay (exponential backoff recommended) · check the error message for details · if it persists, email support@munsit.com with the error code and message, the request, and a timestamp. For external API errors, wait a few minutes and retry. |

## WebSocket errors

The two sockets report errors differently. For **text to speech**, errors arrive as JSON messages on the socket rather than HTTP responses.

```
{
  "type": "error",
  "message": "Error description"
}
```

| Cause | Message |
| --- | --- |
| **Missing authentication** | "x-api-key or Authorization header is required. Provide it in query string, headers, or initConnection message." |
| **Invalid message type** | 'Invalid message type. Expected "voice-request"' |

**Speech-to-Text streaming ([`/api/v1/listen`](/speech-to-text/streaming))** uses a structured `Error` event, followed by a close code.

```
{ "type": "Error", "code": 4002, "message": "Configure.endpointing must be 100..5000 ms", "recoverable": true }
```

`recoverable: false` always precedes a close with the matching code; `recoverable: true` means the session continues.

| Close code | Meaning |
| --- | --- |
| `1000` | Normal close after `CloseStream`. |
| `1008` | Policy rejection: authentication failed, concurrent-session limit reached, or insufficient wallet balance — the `Error` message says which. |
| `1011` | Internal error, or 12 s with no audio and no `KeepAlive`. |
| `4002` | Invalid connection parameters. |
| `4008` | Audio sent more than 60 s ahead of real time. |

## Best practices

Seven habits that keep error handling boring — in the good way.

| Practice | Why |
| --- | --- |
| **Implement retry logic** | For 500-level errors, use exponential backoff. |
| **Handle rate limits** | Monitor for 429 errors and implement request queuing. |
| **Validate inputs** | Prevent 400 errors by validating before sending requests. |
| **Monitor balance** | Check balance before making requests to avoid 402 errors. |
| **Cache API keys** | Store keys securely and handle expiration gracefully. |
| **Log errors** | Log all errors with context for debugging. |
| **User-friendly messages** | Map error codes to messages your users can act on. |

> Hit something not covered here? Email support@munsit.com, or see the Support page for what to include and check the status page for service updates.


---

# MCP server

> These docs are an MCP server. Connect it once and your coding assistant reads the documentation itself — searching it, opening pages, pulling an endpoint's exact request and response schema — instead of you pasting pages into a chat window.

## Connect it

One URL, https://docs.munsit.com/api/mcp. No key, no auth, nothing to install — the docs are public and so is the server.

```
claude mcp add --transport http munsit-docs https://docs.munsit.com/api/mcp
```

```
{
  "mcpServers": {
    "munsit-docs": {
      "url": "https://docs.munsit.com/api/mcp"
    }
  }
}
```

```
# for a client that speaks stdio but not Streamable HTTP
npx -y mcp-remote https://docs.munsit.com/api/mcp
```

```
curl -sX POST https://docs.munsit.com/api/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Response

```
// the server is stateless, so tools/list answers without a handshake
{ "result": { "tools": [ { "name": "search_docs", … } ] } }
```

Streamable HTTP, stateless — there is no session to keep alive and nothing to configure per project. [What is MCP? →](https://modelcontextprotocol.io)

> Using Claude in the browser? Add https://docs.munsit.com/api/mcp as a custom connector under Settings → Connectors. Any client that takes a remote MCP URL will take this one.

## What your assistant gets

Five tools. The first two are the ones it will reach for most: find the right page, then read it.

| Tool | Arguments | What comes back |
| --- | --- | --- |
| **search\_docs** | `query`, `limit` | The pages matching a question, ranked, each with the excerpt that matched. Arabic queries work as well as English. |
| **get\_page** | `path` | One page as Markdown, every code sample included. Takes `/errors`, `errors`, `/errors.md` or a full URL. |
| **list\_pages** | — | The whole documentation index, grouped as the sidebar groups it, one line per page. |
| **list\_endpoints** | `query` | Every REST operation from the OpenAPI spec: method, path, summary. Filterable. |
| **get\_endpoint** | `path`, `method` | One operation's full OpenAPI definition — parameters, request body, responses — with every $ref inlined, so it reads as one self-contained document. |

Ask for a working integration and it goes and gets what it needs:

```
Transcribe a meeting recording with speaker labels, in Python.
Use the Munsit docs MCP server for the exact request and response shapes.
```

## Without MCP

Everything the server exposes is a plain URL too, for a tool that only fetches. Nothing here needs a key either.

- [llms.txt](/llms.txt) — Every page with its one-line description — the index, for orienting before fetching. — `/llms.txt`

- [llms-full.txt](/llms-full.txt) — The whole documentation as one Markdown file. Around 54k tokens, so prefer the MCP server or a single page. — `/llms-full.txt`

- [Any page as Markdown](/errors.md) — Append .md to any URL, or send an Accept: text/markdown header and the same URL returns Markdown. — `/errors.md`

- [OpenAPI & AsyncAPI](/openapi.json) — The REST surface and the WebSocket protocol as specs, for generating a client. — `/openapi.json`

> Reading these docs is all this server does — it has no access to your Munsit account, your keys, or your audio. To call the API itself you still need a key.


---

# Support

> If you need help integrating, deploying, or troubleshooting Munsit, our team is here to assist — especially for enterprise and regulated environments where it has to run reliably in production.

## Contact support

Email [support@munsit.com](mailto:support@munsit.com) with your request and our team will get back to you promptly. You can expect a response within **48–72 hours**.

- [Email support](mailto:support@munsit.com) — Send your request with as much context as you can — the team replies within 48–72 hours. — `support@munsit.com`

> Help us respond faster. Include relevant details such as request IDs, timestamps, deployment type, and a brief description of the issue.

## What we can help with

Anything between "my first request" and "our production deployment" is in scope.

| Topic | Examples |
| --- | --- |
| **API usage & integration** | Endpoints, parameters, response handling, SDK-less integration questions. Start with [Errors](/errors) for anything the API returned. |
| **Deployment support** | SaaS, dedicated, or on-prem deployments — see [Self hosting](/deployment/self-hosting). |
| **Performance or quality issues** | Latency, throughput, transcription or synthesis quality. |
| **Configuration & setup guidance** | Keys, regions, limits, environment setup. |
| **Account & access queries** | Billing, plans, model access, key management. |

## Support plans

If your organization has an **Enterprise or contracted support plan**, your request will be handled according to your agreed SLA. If you are using Munsit without a contracted support plan, you can still reach out via email and our team will assist on a best-effort basis.

| Plan | Handling |
| --- | --- |
| **Enterprise / contracted** | Handled according to your agreed SLA. |
| **No contracted plan** | Best-effort assistance via support@munsit.com. |

> We're committed to making sure Munsit runs reliably in production — especially for enterprise and regulated environments.


---

# Get started

> Munsit Text-to-Speech converts text into high-quality, natural-sounding Arabic audio — built for the MENA region, tuned for very low latency, and fluent across dialects from Abu Dhabi to Rabat.

## What is Munsit TTS?

Munsit Text-to-Speech (TTS) is an advanced AI-powered service that converts text into high-quality audio with exceptional performance characteristics. Built specifically for the Middle East and North Africa (MENA) region, Munsit TTS delivers natural-sounding Arabic speech with very low latency and support for multiple Arabic dialects.

Munsit TTS transforms your text into lifelike audio, enabling you to build voice-enabled applications, interactive systems, and content that speaks naturally in Arabic.

## Key features

Four things Munsit TTS is built around.

| Feature | What it means |
| --- | --- |
| **Ultra-low latency** | Very good latency performance, making Munsit TTS ideal for real-time applications and conversational AI. |
| **Exceptional Arabic dialects** | Optimized for authentic Arabic speech across multiple dialects, from Abu Dhabi to Rabat. |
| **MENA-optimized** | Specifically designed and optimized for the Middle East and North Africa region, ensuring cultural and linguistic accuracy. |
| **High-quality audio** | Natural-sounding speech that captures the nuances of Arabic pronunciation and intonation. |

## Go further

Pick a model, pick a voice, and make your first synthesis call.

- [Synthesize](/text-to-speech/synthesize) — Generate a complete WAV file from Arabic text. — `POST /text-to-speech/{model_id}`

- [Audio streaming output](/text-to-speech/audio-streaming-output) — Stream PCM16 chunks as the audio is generated. — `streaming: true`

- [Voices](/text-to-speech/voices) — Browse the voice library across Fusha, Emirati, Saudi Najdi, Saudi Hijazi and more. — `GET /voices`

- [Models](/text-to-speech/models) — The Faseeh voice synthesis model and its dialects. — `GET /models`


---

# Synthesize

> Generate speech from Arabic text and receive a complete WAV audio file. The entire audio is generated before being returned, ensuring complete audio quality.

## Endpoint

`POST /api/v1/text-to-speech/{model_id}`

Requires API key authentication via the `x-api-key` header. Get a `model_id` from [Models](/text-to-speech/models) and a `voice_id` from [Voices](/text-to-speech/voices).

## Request

**Path parameters**

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model_id` | string | **Yes** | The model identifier to use for generation |

**Request body** — `Content-Type: application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `voice_id` | string | **Yes** | The voice ID to use for synthesis |
| `text` | string | **Yes** | The Arabic text to convert to speech |
| `stability` | number | **Yes** | Voice stability (0.0 to 1.0). Higher values produce more consistent output |
| `speed` | number | No | Speech speed (0.7 to 1.2, default 1.0). Values below 1.0 slow down speech, above 1.0 speed it up |
| `streaming` | boolean | **Yes** | Must be `false` for complete WAV file response |
| `sample_rate` | number | No | Output sample rate in Hz, `8000`–`48000` (default `24000`). **Use `48000` — it's the engine's native rate**, so the audio skips downsampling and needs no client-side resampling for WebRTC. |
| `dialect` | string | No | Dialect hint for synthesis: `auto` (default), `emirati`, or `fusha`. |

## Pauses

Insert a silence of a fixed length anywhere in `text` with a break tag. It works on every text-to-speech endpoint, including [streaming](/text-to-speech/audio-streaming-output) and [word timestamps](/text-to-speech/word-timestamps).

```json
"text": "مرحبا بكم <break time=\"3s\"/> في فصيح"
"text": "أهلا <break time=\"500ms\"/> وسهلا"
```

| Property | Value |
| --- | --- |
| **Syntax** | `<break time="<duration>"/>` |
| **Units** | **Required.** Seconds (`3s`, `1.5s`) or milliseconds (`500ms`) |
| **Max per tag** | `3s` — longer values are clamped down to 3 s |
| **Max per request** | **20** break tags, and about **30 s** of pause in total |
| **Placement** | Anywhere in the text, repeated as needed, within the limits above |

> The unit is mandatory, and a tag without one is silently ignored. <break time="3"/> produces no pause, no error, and is not spoken — the request succeeds and the tag simply disappears. Always write 3s or 500ms.

Every one of these limits degrades quietly rather than returning an error. A `10s` tag yields roughly 3 s of silence; past 20 tags or about 30 s of accumulated pause, further breaks add little or nothing. Nothing in the response tells you a limit was hit, so treat the numbers above as a budget you stay inside rather than something the API will enforce for you.

The pause is rendered by the model as part of the audio, so it lands inside the returned waveform and counts toward the clip's duration.

> Multi-speaker. Break tags are the way to space out speaker turns — put one at the end of a segment's text. There are no separate pause fields on the speakers array.

## Example request

Set `streaming: false` and save the response body as a WAV file. For chunked output, see [Audio streaming output](/text-to-speech/audio-streaming-output).

```bash
curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": false
  }' \
  --output output.wav
```

```python
import requests

url = "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
headers = {
    "x-api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
}
data = {
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": False
}

response = requests.post(url, json=data, headers=headers)

if response.status_code == 200:
    with open("output.wav", "wb") as f:
        f.write(response.content)
else:
    print(f"Error: {response.status_code} - {response.text}")
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    voice_id: 'ar-najdi-male-2',
    text: 'مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم',
    stability: 0.5,
    speed: 1.0,
    streaming: false,
  }),
});

const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
// Use audioUrl to play or download the audio
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "io"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
    payload, _ := json.Marshal(map[string]any{
        "voice_id":  "ar-najdi-male-2",
        "text":      "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
        "stability": 0.5,
        "speed":     1.0,
        "streaming": false,
    })

    req, _ := http.NewRequest("POST", url, bytes.NewReader(payload))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    out, _ := os.Create("output.wav")
    defer out.Close()
    io.Copy(out, resp.Body)
}
```

## Response

**Status code:** `200 OK`. The body is a complete WAV audio file.

| Header | Value |
| --- | --- |
| `Content-Type` | `audio/wav` |
| `Cache-Control` | `no-cache` |
| `Content-Length` | `<file_size>` |

## Error responses

Errors come back as JSON with an `errorCode` and `errorMessage`.

| Status | Error code | Example message |
| --- | --- | --- |
| **400** Bad Request | `400xx` | `Model not found: invalid_model_id` |
| **402** Payment Required | `402xx` | `Insufficient wallet balance. Required: $0.05, Available: $0.02` |

## Cost calculation

The cost is calculated from the **text length** (number of characters) and the **model cost per character**. Cost is deducted from your wallet balance upon successful generation.

> Wallet balance. Ensure your wallet has sufficient balance before making requests. Check your balance in the Munsit dashboard.

## Go further

- [Audio streaming output](/text-to-speech/audio-streaming-output) — Same endpoint with streaming: true — PCM16 chunks as they're generated. — `POST /text-to-speech/{model_id}`

- [Voices](/text-to-speech/voices) — Pick a voice_id from the voice library. — `GET /voices`

- [Models](/text-to-speech/models) — Pick a model_id — quality vs latency. — `GET /models`

- [Voice cloning](/text-to-speech/voice-cloning) — Create a custom voice from your own audio. — `POST /voices/clone`


---

# Audio streaming output

> Generate speech from Arabic text with streaming PCM16 audio output. When streaming: true is set, audio chunks are streamed as they're generated — low-latency delivery you can start playing before generation completes.

## How it works

When streaming is enabled:

| Step | What happens |
| --- | --- |
| **1** | The API starts generating audio immediately |
| **2** | Audio chunks are sent as they become available |
| **3** | You can begin playback before generation completes |
| **4** | Lower latency compared to non-streaming requests |

## Audio format

Streaming responses return raw PCM audio data:

PCM1648000 Hz recommended24000 Hz defaultmono16-bit

| Property | Value |
| --- | --- |
| **Format** | PCM (Pulse Code Modulation) |
| **Sample rate** | Follows `sample_rate`: `48000` Hz recommended (engine-native), `24000` Hz default |
| **Channels** | Mono |
| **Bit depth** | 16-bit |

## Endpoint & request

`POST /api/v1/text-to-speech/{model_id}`

Requires API key authentication via the `x-api-key` header. Same endpoint as [Synthesize](/text-to-speech/synthesize) — the only difference is `streaming` must be `true`.

**Path parameters**

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model_id` | string | **Yes** | The model identifier to use for generation |

**Request body** — `Content-Type: application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `voice_id` | string | **Yes** | The voice ID to use for synthesis |
| `text` | string | **Yes** | The Arabic text to convert to speech |
| `stability` | number | **Yes** | Voice stability (0.0 to 1.0). Higher values produce more consistent output |
| `speed` | number | No | Speech speed (0.7 to 1.2, default 1.0). Values below 1.0 slow down speech, above 1.0 speed it up |
| `streaming` | boolean | **Yes** | Must be `true` for streaming response |
| `sample_rate` | number | No | Output sample rate in Hz, `8000`–`48000` (default `24000`). **Use `48000` — it's the engine's native rate**, so the audio skips downsampling and needs no client-side resampling for WebRTC. |
| `dialect` | string | No | Dialect hint for synthesis: `auto` (default), `emirati`, or `fusha`. |

## Example request

The cURL example uses the `faseeh-v1-preview` model with the `ar-najdi-male-2` voice.

```bash
curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "streaming": true,
    "stability": 0.5,
    "speed": 1
  }' \
  --output audio.pcm
```

```python
import requests

url = "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
headers = {
    "x-api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
}
data = {
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": True
}

response = requests.post(url, json=data, headers=headers, stream=True)

if response.status_code == 200:
    with open("output.pcm", "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            if chunk:
                f.write(chunk)
else:
    print(f"Error: {response.status_code} - {response.text}")
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    voice_id: 'ar-najdi-male-2',
    text: 'مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم',
    stability: 0.5,
    speed: 1.0,
    streaming: true,
  }),
});

const reader = response.body.getReader();
const chunks = [];

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}

// Combine chunks into single audio buffer
const audioBuffer = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0));
let offset = 0;
for (const chunk of chunks) {
  audioBuffer.set(chunk, offset);
  offset += chunk.length;
}
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
    payload, _ := json.Marshal(map[string]any{
        "voice_id":  "ar-najdi-male-2",
        "text":      "مرحبا بك في فصيح",
        "streaming": true,
        "stability": 0.5,
        "speed":     1,
    })

    req, _ := http.NewRequest("POST", url, bytes.NewReader(payload))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    out, _ := os.Create("audio.pcm")
    defer out.Close()

    buf := make([]byte, 8192)
    for {
        n, err := resp.Body.Read(buf)
        if n > 0 {
            out.Write(buf[:n])
        }
        if err != nil {
            break
        }
    }
}
```

## Response

**Status code:** `200 OK`. The body is a stream of mono PCM16 audio chunks at the rate you requested — 24 kHz by default, 48 kHz when you pass `sample_rate=48000` (recommended). The resolved rate is echoed in the `Content-Type` header, so a client can follow it rather than assume.

| Header | Value |
| --- | --- |
| `Content-Type` | `audio/raw;codec=pcm16;rate=<sample_rate>;channels=1` — e.g. `rate=48000` |
| `Cache-Control` | `no-cache` |
| `Connection` | `keep-alive` |

**Handling streaming responses.** Process the streaming PCM data in your application: save chunks to a buffer, play audio chunks as they arrive, and convert PCM to your desired format (WAV, MP3, etc.) if needed.

## Error responses

Errors come back as JSON with an `errorCode` and `errorMessage`.

| Status | Error code | Example message |
| --- | --- | --- |
| **400** Bad Request | `400xx` | `Model not found: invalid_model_id` |
| **402** Payment Required | `402xx` | `Insufficient wallet balance. Required: $0.05, Available: $0.02` |

## Cost calculation

The cost is calculated from the **text length** (number of characters) and the **model cost per character**. Cost is deducted from your wallet balance upon successful generation.

> Wallet balance. Ensure your wallet has sufficient balance before making requests. Check your balance in the Munsit dashboard.

## Go further

- [Synthesize](/text-to-speech/synthesize) — Same endpoint with streaming: false — a complete WAV file. — `POST /text-to-speech/{model_id}`

- [Voices](/text-to-speech/voices) — Pick a voice_id from the voice library. — `GET /voices`

- [Models](/text-to-speech/models) — Faseeh paired with streaming for low-latency, real-time audio. — `GET /models`

- [LiveKit](/integrations/livekit) — Use streaming TTS inside a voice agent. — `plugin`


---

# Word timestamps

> Generate speech and get character-level timings for the text you submitted, in one request. Use it to highlight words as they are spoken, drive captions, or align a transcript to the audio.

## How it works

This endpoint streams **NDJSON** — one JSON object per line — instead of raw PCM. Two kinds of line arrive:

| Line | What it carries |
| --- | --- |
| **Audio** | `audio_base64` — a base64-encoded chunk of PCM16 audio. Decode and append these in order to rebuild the clip. |
| **Alignment** | `alignment` and `normalized_alignment` — character arrays with start and end times. `audio_base64` is empty on these lines. |

> Alignment arrives at the end. Timings are emitted on the final lines, once generation has finished — not incrementally alongside each audio chunk. If you need to highlight from the first word, buffer the whole response before starting playback, or split long text into sentences and request them separately.

## Audio format

The audio is the same PCM16 the streaming endpoint returns — it is simply base64-encoded and split across the `audio_base64` lines instead of being sent as a raw byte stream.

PCM16base64 per line48000 Hz recommended24000 Hz defaultmono16-bit

| Property | Value |
| --- | --- |
| **Format** | PCM (Pulse Code Modulation), signed 16-bit little-endian |
| **Encoding** | Base64, one chunk per NDJSON line |
| **Sample rate** | Follows `sample_rate`: `48000` Hz recommended (engine-native), `24000` Hz default |
| **Channels** | Mono |
| **Chunk size** | Varies — typically a fraction of a second of audio per line |

**To rebuild the clip:** base64-decode each `audio_base64` value and concatenate the bytes **in the order the lines arrive**. The result is headerless PCM — the same bytes the streaming endpoint would have given you. Most players need a container, so prepend a 44-byte WAV header (using your `sample_rate`, 1 channel, 16 bits) before writing a `.wav` file, or feed the samples straight into a Web Audio buffer.

> Don't skip lines. Every audio_base64 value is a contiguous slice of one continuous waveform. Dropping or reordering a line produces audible clicks and shifts everything after it out of sync with the timings.

## Endpoint & request

`POST /api/v1/text-to-speech/{model_id}/with-timestamps`

Requires API key authentication via the `x-api-key` header. Single voice only — the `speakers` array supported by [Synthesize](/text-to-speech/synthesize) is not accepted here.

**Path parameters**

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model_id` | string | **Yes** | The model identifier to use for generation. Must be a model served by the v1.5 engine — see [Models](/text-to-speech/models). |

**Request body** — `Content-Type: application/json`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `voice_id` | string | **Yes** | The voice ID to use for synthesis |
| `text` | string | **Yes** | The Arabic text to convert to speech (max 10,000 characters) |
| `stability` | number | **Yes** | Voice stability (0.0 to 1.0). Higher values produce more consistent output |
| `speed` | number | No | Speech speed (0.7 to 1.2, default 1.0) |
| `sample_rate` | number | No | Output sample rate in Hz, `8000`–`48000` (default `24000`). **Use `48000`** — it's the engine's native rate, so the audio skips downsampling. |
| `dialect` | string | No | Dialect hint for synthesis: `auto` (default), `emirati`, or `fusha`. |

## Example request

The examples use the `faseeh-v1-preview` model with the `ar-najdi-male-2` voice, and rebuild both the audio and the word list.

```bash
curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview/with-timestamps" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "sample_rate": 48000
  }' \
  --output stream.ndjson
```

```python
import base64, json, wave, requests

url = "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview/with-timestamps"
headers = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}
data = {
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "sample_rate": 48000,
}

pcm = bytearray()
alignment = None

with requests.post(url, json=data, headers=headers, stream=True) as r:
    r.raise_for_status()
    for line in r.iter_lines():
        if not line:
            continue
        msg = json.loads(line)
        if msg.get("audio_base64"):
            pcm += base64.b64decode(msg["audio_base64"])
        if msg.get("alignment"):
            alignment = msg["alignment"]

# Group characters into words
words, cur = [], None
for ch, s, e in zip(alignment["characters"],
                     alignment["character_start_times_seconds"],
                     alignment["character_end_times_seconds"]):
    if ch.isspace():
        cur = None
        continue
    if cur is None:
        cur = {"text": "", "start": s, "end": e}
        words.append(cur)
    cur["text"] += ch
    cur["end"] = e

print(words)

# Wrap the concatenated PCM in a WAV container so it can be played
with wave.open("output.wav", "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)  # 16-bit
    w.setframerate(48000)  # must match sample_rate
    w.writeframes(bytes(pcm))
```

```javascript
const res = await fetch(
  'https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview/with-timestamps',
  {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      voice_id: 'ar-najdi-male-2',
      text: 'مرحبا بك في فصيح',
      stability: 0.5,
      sample_rate: 48000,
    }),
  }
);

const reader = res.body.getReader();
const decoder = new TextDecoder();
const chunks = [];
let alignment = null;
let buffer = '';

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  let i;
  while ((i = buffer.indexOf('\n')) !== -1) {
    const line = buffer.slice(0, i).trim();
    buffer = buffer.slice(i + 1);
    if (!line) continue;
    const msg = JSON.parse(line);
    if (msg.audio_base64) chunks.push(Buffer.from(msg.audio_base64, 'base64'));
    if (msg.alignment) alignment = msg.alignment;
  }
}

// Headerless PCM16 — same bytes the streaming endpoint returns
const pcm = Buffer.concat(chunks);
// Prepend a 44-byte WAV header (1 channel, 16-bit, sample_rate) to play it,
// or copy the samples into an AudioBuffer in the browser.
```

## Response

**Status code:** `200 OK`. The body is a newline-delimited JSON stream.

| Header | Value |
| --- | --- |
| `Content-Type` | `application/x-ndjson;charset=utf-8;rate=<sample_rate>` |
| `Cache-Control` | `no-cache` |

A complete response looks like this — audio lines first, then the alignment lines. Every line carries all four keys, with `null` where a field does not apply:

```json
// audio — one chunk per line, in playback order
{"audio_base64": "XQACASUBAgERATUBYAFnAVkBcQGPAXYB…", "alignment": null, "normalized_alignment": null, "quality_check": null}
{"audio_base64": "o+Ii5Tro1+sT8FL0qvhP/SYBhgMOBvUJ…", "alignment": null, "normalized_alignment": null, "quality_check": null}
// … more audio lines …

// then the timings — audio_base64 is empty from here on
{"audio_base64": "", "alignment": null, "normalized_alignment": {"characters": […], "character_start_times_seconds": […], "character_end_times_seconds": […]}, "quality_check": null}
{"audio_base64": "", "alignment": {"characters": […], "character_start_times_seconds": […], "character_end_times_seconds": […]}, "normalized_alignment": null, "quality_check": null}
```

A short clip like مرحبا بك في فصيح at 48 kHz comes back as roughly a dozen audio lines followed by the two alignment lines. Read to the end of the stream: closing early loses the timings entirely.

**Line fields**

| Field | Type | Description |
| --- | --- | --- |
| `audio_base64` | string | Base64-encoded PCM16 mono audio at the requested `sample_rate`. Empty on alignment lines. |
| `alignment` | object | Character timings aligned to **the text you submitted**. Use this one to map timings back onto your own string. |
| `normalized_alignment` | object | Character timings aligned to the engine's normalized (and, for MSA, diacritized) form of the text. Does not match your input character-for-character — see the warning below. |

Alongside the three timing arrays, each alignment object carries quality flags:

| Field | On | Meaning |
| --- | --- | --- |
| `aligned` | both | Whether alignment succeeded. If `false`, treat the timings as unreliable and fall back to plain playback. |
| `mapped` | `alignment` | Per character: whether it was mapped back onto your original text. Characters the engine could not place are `false`. |
| `anchored` | `normalized_alignment` | Per character: whether the timing is anchored to real audio rather than interpolated between neighbours. Whitespace is typically `false`. |
| `coverage` | `normalized_alignment` | Proportion of characters that are anchored, `0`–`1`. `1.0` means every character got a real timing. |

For highlighting, the practical rule is: bail out if `aligned` is `false`, and skip any word whose characters are all unanchored — its timing is a guess, and highlighting it will look wrong against the audio.

Both alignment objects share the same shape — three parallel arrays of equal length:

```json
{
  "characters": ["م", "ر", "ح", "ب", "ا", " ", "ب", "ك"],
  "character_start_times_seconds": [0.0, 0.132, 0.244, 0.366, 0.477, 0.528, 0.610, 0.701],
  "character_end_times_seconds": [0.132, 0.244, 0.366, 0.477, 0.528, 0.610, 0.701, 0.853],
  "aligned": true
}
```

If your text contains a [break tag](/text-to-speech/synthesize#pauses), it stays in `alignment` exactly as you wrote it, with timings spanning the silence it produces — so offsets still line up with your string. It is **not** present in `normalized_alignment`, which describes the spoken form only. When grouping into words, treat the whole tag as one unit: splitting on whitespace alone tears it into `<break` and `time="1s"/>`.

Concatenating `alignment.characters` reproduces your input string **exactly**, so index `i` in those arrays is index `i` in your text — that is what makes it safe to map timings back onto your own string.

> That guarantee applies to alignment only. normalized_alignment describes the engine's spoken form, which can differ in length and content — numerals are expanded, so تأسست الشركة في 1985 (20 characters) becomes تأسست الشركة في ألف وتسعمئة وخمسة وثمانين (41 characters). Its indices do not map onto your input. Use it to read what was actually spoken, not to highlight your own text.

**Timings are per character, not per word** — there is no word array. Derive words by walking the arrays and breaking on whitespace: a word's start is its first character's start time, its end is its last character's end time. The Python example above does exactly this.

## Error responses

Errors come back as JSON with an `errorCode` and `errorMessage`.

| Status | Error code | Example message |
| --- | --- | --- |
| **400** Bad Request | `400xx` | `Word timestamps are not available for model 'x'. Use a model served by the v1.5 engine.` |
| **401** Unauthorized | `40101` | `Authorization required. Provide Authorization Bearer token (Clerk) or x-api-key header.` |
| **402** Payment Required | `402xx` | `Insufficient wallet balance. Required: $0.05, Available: $0.02` |

## Cost calculation

Priced exactly like a standard synthesis request — from the **text length** and the **model cost per character**. Timestamps add no extra charge. The generation appears in your history the same way a streaming request does.

## Go further

- [Synthesize](/text-to-speech/synthesize) — Standard generation without timings. — `POST /text-to-speech/{model_id}`

- [Audio streaming output](/text-to-speech/audio-streaming-output) — Raw PCM streamed as it is generated. — `POST /text-to-speech/{model_id}`

- [Models](/text-to-speech/models) — Check which models the v1.5 engine serves. — `GET /models`

- [Voices](/text-to-speech/voices) — Pick a voice_id from the voice library. — `GET /voices`


---

# Voices

> Munsit offers a variety of voices with different accents, including Fusha, Emirati, Saudi Najdi, Saudi Hijazi, and more. List them all with one call, then use the voice_id in any text-to-speech request.

## List voices

`GET /api/v1/voices`

Retrieve a list of all available voices for text-to-speech synthesis. Requires API key authentication via the `x-api-key` header. Returns an array of voice objects.

```bash
curl -X GET "https://api.munsit.com/api/v1/voices" \
  -H "x-api-key: YOUR_API_KEY"
```

```python
import requests

url = "https://api.munsit.com/api/v1/voices"
headers = {
    "x-api-key": "YOUR_API_KEY"
}

response = requests.get(url, headers=headers)
voices = response.json()
print(voices)
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/voices', {
  method: 'GET',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
  },
});

const voices = await response.json();
console.log(voices);
```

```go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/voices"
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var voices []map[string]any
    json.NewDecoder(resp.Body).Decode(&voices)
    fmt.Println(voices)
}
```

## Response fields

Each voice object contains:

| Field | Type | Description |
| --- | --- | --- |
| `voice_id` | string | Unique identifier for the voice (used in text-to-speech requests) |
| `name` | string | Human-readable name of the voice |
| `description` | string | null | Detailed description of the voice characteristics |
| `gender` | string | null | Gender of the voice (`male`, `female`, or `null`) |
| `age` | string | null | Age category of the voice (`middle`, `elderly`, or `null`) |
| `languages` | array\[string\] | List of language codes supported by the voice (e.g., `["ar", "en"]`) |
| `dialect` | array\[string\] | List of dialects supported by the voice (e.g., `["fusha", "emirati", "najdi"]`) |
| `type` | string | null | Voice type (`neural` or `null`) |
| `sample_url` | string | URL to an audio sample of the voice |

> Treat voice_id as an opaque string. A few ids are readable (ar-najdi-male-2), but most of the catalogue looks like PCtWbxjoNTpVQ6gIPaVZ2Hqm. There is no guaranteed ar-{dialect}-{gender}-{n} convention, and readable ids are not enumerable — neighbouring numbers are not guaranteed to exist. Always list voices with GET /voices and use the ids it returns; never construct or pattern-match one.
> Custom voices. Some voices may have null values for certain fields. These are typically custom user-created voices. The voice_id can still be used in text-to-speech requests regardless of these field values.

## Voice types

Voices can be categorized by **dialect**, **gender** (`male` or `female`), **age** (`middle` or `elderly`) and **languages** (supported language codes, e.g. `ar` for Arabic, `en` for English).

fushaemiratinajdihijazikuwaitibritish…

`fusha` is Modern Standard Arabic; `najdi` and `hijazi` are the Saudi dialects.

## Featured voices

Below are the featured Munsit voices available in the text-to-speech API. Each name links to an audio sample.

| Name | Dialect | Language | Sample |
| --- | --- | --- | --- |
| **Fahad** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/samples/ar-fahad-male.wav) |
| **Lama** | Hijazi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/samples/ar-lama-female.wav) |
| **James** | American | English - Arabic | [Listen](https://pub-416e4565a11a4a838ddb6bd06095ea75.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/xqSiYjA5a4Y1PoHRTW99v3FA.wav) |
| **Jake** | American | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/qnwatnZiPm1XgtUpp1l0pjuQ.wav) |
| **Mansour** | Emirati | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/WpB2Nltu72GF4lcZl9yi62D4.wav) |
| **Maha** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/IPK8qQ3F5NMiQLWFz1a83TG3.wav) |
| **Faisal** | Najdi | English - Arabic | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/8jo3glQpNtAdvRHIyNN8now2.wav) |
| **Turki** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/jEF6Tjsxg3rJhJijqItKNNey.wav) |
| **Reem** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/ybQaNl0nzt9TjN3Oh1zzyNgp.wav) |
| **Mishari** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/MvC2GIG9tT9xvPcCWjILXqkM.wav) |
| **Hala** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/08XOzRjaaumxbHhcGOrWkJ7z.wav) |
| **May** | British | English - Arabic | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/generations/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/e824f251-d000-451c-94c5-91c42af35650.wav) |
| **Latifa** | Emirati | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/generations/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/440c7554-d1b5-4273-b83b-1c4c696eca61.wav) |
| **Mishal** | Najdi | Arabic - English | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/voices/user_36IjgcsiPYmjwpTzdvy0Ew687Gi/DsNnaed3aGVyZZ8iFgXDK0gS.wav) |
| **Maya** | American | English - Arabic | [Listen](https://pub-a2eeb500293c41df9e06b82b4178468a.r2.dev/samples/en-Maya_woman.wav) |

> Explore all voices. View the complete voice library in the dashboard.

## Using a voice

Use the `voice_id` from the response in text-to-speech generation endpoints: POST `/text-to-speech/:model_id` (include `voice_id` in the request body) or WS `/text-to-speech` (include `voice_id` in the WebSocket message). This example uses the `faseeh-v1-preview` model with the `ar-najdi-male-2` voice.

```bash
curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": true
  }' \
  --output audio.pcm
```

```python
import requests

url = "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
headers = {
    "x-api-key": "YOUR_API_KEY",
    "Content-Type": "application/json"
}
data = {
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": True
}

response = requests.post(url, json=data, headers=headers, stream=True)
with open("audio.pcm", "wb") as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    voice_id: 'ar-najdi-male-2',
    text: 'مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم',
    stability: 0.5,
    speed: 1.0,
    streaming: true,
  }),
});

const reader = response.body.getReader();
const chunks = [];

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  chunks.push(value);
}
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "io"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview"
    payload, _ := json.Marshal(map[string]any{
        "voice_id":  "ar-najdi-male-2",
        "text":      "مرحبا بك في فصيح كيف يمكنني مساعدتك اليوم",
        "stability": 0.5,
        "speed":     1.0,
        "streaming": true,
    })

    req, _ := http.NewRequest("POST", url, bytes.NewReader(payload))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    out, _ := os.Create("audio.pcm")
    defer out.Close()
    io.Copy(out, resp.Body)
}
```

> Caching. Voice information doesn't change frequently. Consider caching the voice list to reduce API calls and improve application performance.

## Go further

- [Synthesize](/text-to-speech/synthesize) — Use a voice_id to generate a complete WAV file. — `POST /text-to-speech/{model_id}`

- [Preview a voice](/text-to-speech/voice-preview) — Hear a cloned voice before creating it permanently. — `POST /voices/preview`

- [Voice cloning](/text-to-speech/voice-cloning) — Create a custom voice from your own audio. — `POST /voices/clone`

- [Models](/text-to-speech/models) — Choose the model that pairs with your voice. — `GET /models`


---

# Preview a voice

> Generate a preview of a cloned voice from a source audio file. Upload the audio, provide the text to speak, and stream back PCM16 audio — before committing to a permanent voice.

## Endpoint

`POST /api/v1/voices/preview`

Requires API key authentication via the `x-api-key` header. The request is `multipart/form-data`.

## Request

**Form data** — `Content-Type: multipart/form-data`

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | **Yes** | Text to generate preview with (minimum 3 words, 10 characters) |
| `similarity` | number | **Yes** | Voice similarity to source (0.0 to 1.0). Higher values produce more similar voice |
| `model_id` | string | **Yes** | The model identifier to use for generation |
| `speed` | number | No | Speech speed (0.7 to 1.2, default 1.0). Values below 1.0 slow down speech, above 1.0 speed it up |
| `file` | File | **Yes** | Audio file containing the voice to preview |
| `sample_rate` | number | No | Output sample rate in Hz, `8000`–`48000` (default `24000`). **Use `48000` — it's the engine's native rate**, so the audio skips downsampling and needs no client-side resampling for WebRTC. |

> Notes. The API will process the provided audio file to generate the voice preview. text must be at least 3 words and 10 characters long. The default stays 24000 for backward compatibility — pass sample_rate=48000 for the best quality.

## Example request

Upload a source recording and stream the preview audio back.

```bash
curl -X POST "https://api.munsit.com/api/v1/voices/preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "file=@source_audio.wav" \
  -F "text=مرحبا بك في فصيح، هذا صوتي الجديد" \
  -F "similarity=0.8" \
  -F "model_id=faseeh-v1-preview" \
  -F "speed=1.0" \
  -F "sample_rate=48000" \
  --output voice_preview.pcm
```

```python
import requests

url = "https://api.munsit.com/api/v1/voices/preview"
headers = {
    "x-api-key": "YOUR_API_KEY"
}

with open("source_audio.wav", "rb") as audio_file:
    files = {"file": audio_file}
    data = {
        "text": "مرحبا بك في فصيح، هذا صوتي الجديد",
        "similarity": "0.8",
        "model_id": "faseeh-v1-preview",
        "speed": "1.0"
    }
    response = requests.post(url, files=files, data=data, headers=headers, stream=True)

if response.status_code == 200:
    # Save streaming audio
    with open("voice_preview.pcm", "wb") as f:
        for chunk in response.iter_content(chunk_size=8192):
            f.write(chunk)
    print("Voice preview saved successfully")
else:
    print(f"Error: {response.status_code} - {response.text}")
```

```javascript
import fs from "node:fs";

const formData = new FormData();
const audioFile = new Blob([fs.readFileSync("source_audio.wav")]);
formData.append("file", audioFile, "source_audio.wav");
formData.append("text", "مرحبا بك في فصيح، هذا صوتي الجديد");
formData.append("similarity", "0.8");
formData.append("model_id", "faseeh-v1-preview");
formData.append("speed", "1.0");

const response = await fetch('https://api.munsit.com/api/v1/voices/preview', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    // Note: Content-Type is set automatically by FormData
  },
  body: formData,
});

if (response.ok) {
  // Handle streaming audio response
  const reader = response.body.getReader();
  const audioChunks = [];

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    audioChunks.push(value);
  }

  // Combine chunks and create audio blob
  const audioBlob = new Blob(audioChunks, { type: 'audio/raw' });
  const audioUrl = URL.createObjectURL(audioBlob);
  // Use audioUrl to play the preview
} else {
  const error = await response.json();
  console.error('Error:', error);
}
```

```go
package main

import (
    "bytes"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/voices/preview"

    var body bytes.Buffer
    writer := multipart.NewWriter(&body)

    file, _ := os.Open("source_audio.wav")
    defer file.Close()
    part, _ := writer.CreateFormFile("file", "source_audio.wav")
    io.Copy(part, file)

    writer.WriteField("text", "مرحبا بك في فصيح، هذا صوتي الجديد")
    writer.WriteField("similarity", "0.8")
    writer.WriteField("model_id", "faseeh-v1-preview")
    writer.WriteField("speed", "1.0")
    writer.Close()

    req, _ := http.NewRequest("POST", url, &body)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", writer.FormDataContentType())

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    out, _ := os.Create("voice_preview.pcm")
    defer out.Close()
    io.Copy(out, resp.Body)
}
```

## Response

**Status code:** `200 OK`. The body is streaming PCM16 audio, mono, 16-bit, at the rate you requested — `24000` Hz by default, or `48000` when you pass `sample_rate=48000` (recommended). The resolved rate is always echoed in the `Content-Type` header, so a client can follow it rather than assume.

PCM1648000 Hz recommended24000 Hz defaultmono16-bit

| Header | Value |
| --- | --- |
| `Content-Type` | `audio/raw;codec=pcm16;rate=<sample_rate>;channels=1` — e.g. `rate=48000` |
| `Cache-Control` | `no-cache` |
| `Connection` | `keep-alive` |

## Error responses

Errors come back as JSON with an `errorCode` and `errorMessage`.

| Status | Error code | Example message |
| --- | --- | --- |
| **400** Bad Request | `400xx` | `text is required` |
| **400** Bad Request | `400xx` | `similarity must be a number between 0 and 1` |
| **400** Bad Request | `400xx` | `speed must be a number between 0.7 and 1.2` |
| **400** Bad Request | `400xx` | `model_id is required` |
| **400** Bad Request | `400xx` | `file is required and must be of audio type` |
| **401** Unauthorized | `40101` | `Invalid or missing API key` |
| **402** Payment Required | `40201` | `Insufficient wallet balance` |
| **500** Internal Server Error | `50001` | `Failed to generate voice preview` |

> Voice preview. This endpoint generates a preview of a cloned voice. If you're satisfied with the preview, you can proceed to create it as a permanent voice using the voice creation endpoint.

## Go further

- [Voice cloning](/text-to-speech/voice-cloning) — Happy with the preview? Create the voice permanently. — `POST /voices/clone`

- [Voices](/text-to-speech/voices) — Browse the existing voice library. — `GET /voices`

- [Models](/text-to-speech/models) — Pick a model_id for the preview generation. — `GET /models`

- [Synthesize](/text-to-speech/synthesize) — Use your new voice in a text-to-speech request. — `POST /text-to-speech/{model_id}`


---

# Voice cloning

> Create custom Arabic voices from audio samples. Preview a voice first, then commit the clone — the returned voice_id works everywhere a stock voice does.

## What voice cloning gives you

Voice cloning lets you create lifelike custom voices from short audio samples, clone voices across different Arabic dialects, use them in text-to-speech generation, and manage your own voice library — all with the same API key.

| Capability | What it means |
| --- | --- |
| **High-quality cloning** | Create lifelike voice clones from short audio samples. |
| **Multi-dialect support** | Clone voices across different Arabic dialects. |
| **Voice library** | Manage and organize your custom voices alongside stock ones. |
| **Easy integration** | Use cloned voices seamlessly with the TTS API — just pass the `voice_id`. |

## The preview-first flow

Cloning is a two-step process. You first generate a preview with the [Voice Preview API](/text-to-speech/voice-preview), listen to it, and only then commit the clone. The clone request needs **both** files: the preview audio and the original sample it was built from.

| Step | What happens |
| --- | --- |
| **1 · Upload a sample** | Provide a high-quality audio sample of the voice you want to clone. |
| **2 · Preview** | Munsit processes the sample and returns a generated preview audio file. |
| **3 · Clone** | Send the preview file, the original file and the preview text to `/voices/clone`. A unique `voice_id` is assigned automatically. |
| **4 · Use in TTS** | Pass the `voice_id` to any [text-to-speech](/text-to-speech/synthesize) request. The voice is available immediately after creation. |

> Sample quality matters. Use high-quality audio (minimum 1 minute recommended) with clear, natural speech, recorded in a quiet environment with minimal background noise. Multiple samples produce better voice quality.

## Endpoint

`POST /api/v1/voices/clone`

Authenticated with the `x-api-key` header. The body is `multipart/form-data`; the content type is set automatically when you use FormData or file uploads.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `voice_file` | File | **Yes** | The generated preview audio file from the preview API. |
| `reference_audio_file` | File | **Yes** | The original audio file used for the preview. |
| `text` | string | **Yes** | The text used in preview generation — must match what you sent to the preview API. |
| `stability` | number | **Yes** | Voice stability (0.0 to 1.0). Higher values produce more consistent output. |
| `name` | string | **Yes** | Name for the cloned voice. |
| `model` | string | **Yes** | Model identifier to use for voice cloning. |
| `description` | string | No | Description of the voice. |
| `gender` | string | No | Gender of the voice (e.g., `male`, `female`). |
| `age` | string | No | Age category of the voice (e.g., `middle`, `elderly`). |
| `languages` | string | No | Comma-separated list of language codes (e.g., `ar,en`). |
| `dialects` | string | No | Comma-separated list of dialects (e.g., `najdi,hijazi`). |
| `avatar_url` | string | No | URL to an avatar image for the voice. |

## Clone a voice

Same request in three languages. `voice_file` is the preview output; `reference_audio_file` is the original recording; `text` matches the preview text.

```bash
curl -X POST "https://api.munsit.com/api/v1/voices/clone" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "voice_file=@voice_sample.wav" \
  -F "reference_audio_file=@reference_audio.wav" \
  -F "text=مرحبا بك في فصيح، هذا صوتي المستنسخ" \
  -F "stability=0.8" \
  -F "name=My Cloned Voice" \
  -F "model=faseeh-v1-preview" \
  -F "description=A custom cloned voice" \
  -F "gender=male" \
  -F "languages=ar,en" \
  -F "dialects=najdi"
```

```python
import requests

url = "https://api.munsit.com/api/v1/voices/clone"
headers = {"x-api-key": "YOUR_API_KEY"}

files = {
    "voice_file": open("voice_sample.wav", "rb"),
    "reference_audio_file": open("reference_audio.wav", "rb")
}

data = {
    "text": "مرحبا بك في فصيح، هذا صوتي المستنسخ",
    "stability": "0.8",
    "name": "My Cloned Voice",
    "model": "faseeh-v1-preview",
    "description": "A custom cloned voice",
    "gender": "male",
    "languages": "ar,en",
    "dialects": "najdi"
}

response = requests.post(url, files=files, data=data, headers=headers)

if response.status_code == 200:
    voice = response.json()
    print(f"Voice created: {voice['voice_id']}")
else:
    print(f"Error: {response.status_code} - {response.text}")
```

```javascript
import fs from "node:fs";

const formData = new FormData();
const voiceFile = new Blob([fs.readFileSync("voice_sample.wav")]);
const referenceAudioFile = new Blob([fs.readFileSync("reference_audio.wav")]);

formData.append("voice_file", voiceFile, "voice_sample.wav");
formData.append("reference_audio_file", referenceAudioFile, "voice_sample.wav");
formData.append("text", "مرحبا بك في فصيح، هذا صوتي المستنسخ");
formData.append("stability", "0.8");
formData.append("name", "My Cloned Voice");
formData.append("model", "faseeh-v1-preview");
formData.append("description", "A custom cloned voice");
formData.append("gender", "male");
formData.append("languages", "ar,en");
formData.append("dialects", "najdi");

const response = await fetch('https://api.munsit.com/api/v1/voices/clone', {
  method: 'POST',
  headers: { 'x-api-key': 'YOUR_API_KEY' }, // Content-Type set by FormData
  body: formData,
});

if (response.ok) {
  const voice = await response.json();
  console.log('Voice created:', voice.voice_id);
} else {
  const error = await response.json();
  console.error('Error:', error);
}
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/voices/clone"

    var body bytes.Buffer
    writer := multipart.NewWriter(&body)

    for field, path := range map[string]string{
        "voice_file":           "voice_sample.wav",
        "reference_audio_file": "reference_audio.wav",
    } {
        file, _ := os.Open(path)
        part, _ := writer.CreateFormFile(field, path)
        io.Copy(part, file)
        file.Close()
    }

    fields := map[string]string{
        "text":        "مرحبا بك في فصيح، هذا صوتي المستنسخ",
        "stability":   "0.8",
        "name":        "My Cloned Voice",
        "model":       "faseeh-v1-preview",
        "description": "A custom cloned voice",
        "gender":      "male",
        "languages":   "ar,en",
        "dialects":    "najdi",
    }
    for k, v := range fields {
        writer.WriteField(k, v)
    }
    writer.Close()

    req, _ := http.NewRequest("POST", url, &body)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", writer.FormDataContentType())

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var voice map[string]any
    json.NewDecoder(resp.Body).Decode(&voice)
    fmt.Println("Voice created:", voice["voice_id"])
}
```

## Response

**200 OK**, `application/json`. The clone is available immediately — use `voice_id` in any TTS request.

```json
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "voice_id": "ar-cloned-voice-1",
  "name": "My Cloned Voice",
  "description": "A custom cloned voice",
  "gender": "male",
  "age": "middle",
  "languages": ["ar", "en"],
  "dialect": ["najdi"],
  "type": "neural",
  "sample_url": "https://example.com/voices/user123/ar-cloned-voice-1.wav",
  "avatar_url": null,
  "stability": 0.8
}
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (UUID) | Unique identifier for the voice record. |
| `voice_id` | string | Voice identifier used in API calls. |
| `name` | string | Name of the cloned voice. |
| `description` | string | null | Description of the voice. |
| `gender` | string | null | Gender of the voice. |
| `age` | string | null | Age category of the voice. |
| `languages` | string\[\] | List of language codes supported by the voice. |
| `dialect` | string\[\] | List of dialects supported by the voice. |
| `type` | string | null | Voice type. |
| `sample_url` | string | URL to the sample audio file. |
| `avatar_url` | string | null | URL to the avatar image. |
| `stability` | number | Voice stability value. |

## Errors

Validation failures come back as **400** with a specific `errorMessage`; auth and processing failures use the shared error shape.

| Status | errorCode | errorMessage |
| --- | --- | --- |
| **400** | `400xx` | `voice_file is required and must be a file` |
| **400** | `400xx` | `reference_audio_file is required and must be a file` |
| **400** | `400xx` | `name is required` · `text is required` · `model is required` |
| **400** | `400xx` | `stability must be a number between 0 and 1` |
| **401** | `40101` | `Invalid or missing API key` |
| **500** | `50001` | `Failed to process voice file` · `Failed to upload voice file` |

## Go further

The clone is just a `voice_id`. Everything downstream is regular TTS.

- [Preview a voice](/text-to-speech/voice-preview) — Generate the preview file this endpoint requires — step one of cloning. — `POST /voices/preview`

- [Synthesize speech](/text-to-speech/synthesize) — Use your new voice_id in a text-to-speech request. — `POST /text-to-speech/{model_id}`

- [List voices](/text-to-speech/voices) — See stock voices and your cloned ones in a single library. — `GET /voices`

- [Stream audio out](/text-to-speech/audio-streaming-output) — Cloned voices work over the streaming WebSocket too. — `WSS /websocket/text-to-speech`


---

# Audio narrative

> Turn any text into a fully hosted, embeddable audio experience — a branded player you can drop into any page with a single <script> tag. Paste your text, pick a voice, customize the look, done.

## What you can do

Audio Narrative converts written content into a rich audio experience, complete with a player you host nowhere and style everywhere.

| Capability | Detail |
| --- | --- |
| **Generate speech** | High-quality Arabic speech from any text, up to **10,000 characters**. |
| **Pick a voice** | Choose from multiple Munsit voices and models. |
| **Brand the player** | Customize colors and theme to match your site. |
| **Embed anywhere** | One `<script>` tag on any website. |
| **Manage history** | Revisit and tweak all generated narratives from your history. |

> Optimized for Arabic. For best results, make sure your input text is clean, well-punctuated Arabic. Consider running it through Tashkīl first for precise pronunciation.

## Create a narrative

Audio Narrative lives in the [Munsit dashboard](https://app.munsit.com/) — no API call needed. From the sidebar, open **Audio Narrative** and click **Create New**.

1

## Fill in page details

**Title** — the headline shown on the player (e.g. article title). **Author** — the byline beneath the title. **Text** — paste the content to read aloud (up to 10,000 characters). **Model** — `faseeh-v1-preview` (Faseeh). **Stability** — higher for a consistent, neutral delivery; lower for more expressive variation. **Speed** — 0.7× (slower) to 1.2× (faster). **Voice** — pick the one that fits your content.

2

## Customize the player

Switch to **Player Customization**: choose the **Default** or **Light** theme, then pick any hex color for the **background** and one for **text** (titles, controls, timestamps). A live preview updates in real time.

3

## Generate and embed

Click **Create**. Once ready, open the narrative from your history to preview the audio with the built-in player, tweak colors or theme further, and copy the embed code — a single `<script>` tag — into any webpage.

## The embed code

After generating a narrative, copy the embed code from the detail page and paste it into your HTML. Everything is configured through `data-*` attributes.

```
<script
  src="https://widget.munsit.com/audio-narrative@0.1.0-beta.js"
  data-audio="YOUR_AUDIO_URL"
  data-title="Your Article Title"
  data-author="Author Name"
  data-bg="#ffffff"
  data-color="#000000"
  data-theme="light"
></script>
```

| Attribute | Description |
| --- | --- |
| `data-audio` | URL of the generated audio file. |
| `data-title` | Title shown on the player. |
| `data-author` | Author byline shown on the player. |
| `data-bg` | Background color (hex). |
| `data-color` | Text and icon color (hex). |
| `data-theme` | `default` or `light`. |

## Generation parameters

Two dials control how the narrator sounds.

| Parameter | Range | Effect |
| --- | --- | --- |
| **Stability** | `0 – 100%` | Controls delivery consistency; higher = more neutral, lower = more expressive. |
| **Speed** | `0.7× – 1.2×` | Adjusts how fast the narrator speaks. |

## Go further

Prefer raw audio over a hosted player? Those voices are one API call away.

- [Synthesize speech](/text-to-speech/synthesize) — Generate the audio yourself and host it your way. — `POST /text-to-speech`

- [Browse voices](/text-to-speech/voices) — Find the narrator voice that fits your content. — `GET /voices`

- [Clone a voice](/text-to-speech/voice-cloning) — Narrate articles in your own custom voice. — `POST /voices/clone`

- [Tashkīl](/text-to-speech/tashkil) — Diacritize Fusha text before narration for precise reading. — `POST /tashkil/diacritize`


---

# Tashkīl

> Add Arabic diacritics to unvoweled text. Send plain Fusha, get back the same text with the vowel marks needed for precise reading — ready for narration, learning tools, or TTS.

## Why diacritize

Tashkīl adds Arabic diacritics to unvoweled text, making written content clearer for pronunciation, narration, learning, and downstream speech workflows. The model is especially well suited to **Modern Standard Arabic (Fusha)**, where it preserves formal wording while adding the vowel marks needed for more precise reading.

| Included | Typical use case |
| --- | --- |
| Arabic diacritization for plain text | Preparing Fusha scripts for narration and text-to-speech |
| Strong performance on Fusha and formal Arabic | Supporting Arabic reading and pronunciation tools |
| Synchronous API response — simple app flows | Normalizing formal Arabic text before publishing or review |
| Output usable before TTS or content review | Cleaner input for [Synthesize](/text-to-speech/synthesize) and [Audio narrative](/text-to-speech/audio-narrative) |

> Best on formal Arabic. Dialectal, noisy, or highly informal text may need additional review after diacritization.

## Endpoint

`POST /api/v1/tashkil/diacritize`

Authenticated with the `x-api-key` header, JSON body in, JSON out. One field, one round trip.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | **Yes** | Arabic text to diacritize. |

## Diacritize text

Send unvoweled Arabic; read `data.diacritized_text` from the response.

```bash
curl 'https://api.munsit.com/api/v1/tashkil/diacritize' \
  -H 'Content-Type: application/json' \
  -H 'x-api-key: YOUR_MUNSIT_API_KEY' \
  -d '{
    "text": "ذهب الطالب الى المدرسة"
  }'
```

```python
import requests

response = requests.post(
    "https://api.munsit.com/api/v1/tashkil/diacritize",
    headers={"x-api-key": "YOUR_MUNSIT_API_KEY"},
    json={"text": "ذهب الطالب الى المدرسة"},
)

result = response.json()
print(result["data"]["diacritized_text"])
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/tashkil/diacritize', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'YOUR_MUNSIT_API_KEY',
  },
  body: JSON.stringify({ text: 'ذهب الطالب الى المدرسة' }),
});

const result = await response.json();
console.log(result.data.diacritized_text);
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    body, _ := json.Marshal(map[string]string{"text": "ذهب الطالب الى المدرسة"})

    req, _ := http.NewRequest("POST", "https://api.munsit.com/api/v1/tashkil/diacritize", bytes.NewReader(body))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var result map[string]any
    json.NewDecoder(resp.Body).Decode(&result)
    data := result["data"].(map[string]any)
    fmt.Println(data["diacritized_text"])
}
```

Response

```
{
  "statusCode": 200,
  "data": {
    "original_text": "ذهب الطالب الى المدرسة",
    "diacritized_text": "ذَهَبَ الطَّالِبُ إِلَى المَدْرَسَةِ"
  },
  "message": "Success"
}
```

Output

```
ذَهَبَ الطَّالِبُ إِلَى المَدْرَسَةِ
```

Output

```
ذَهَبَ الطَّالِبُ إِلَى المَدْرَسَةِ
```

Output

```
ذَهَبَ الطَّالِبُ إِلَى المَدْرَسَةِ
```

## Response fields

The response wraps both the original and the diacritized text, so you can diff or display them side by side.

| Field | Type | Description |
| --- | --- | --- |
| `statusCode` | number | HTTP-style status code. |
| `data.original_text` | string | Original input text. |
| `data.diacritized_text` | string | Text with Arabic diacritics. |
| `message` | string | Request status message. |

## Errors

Errors follow the shared `errorCode` / `errorMessage` shape — for example `{"errorCode": 40101, "errorMessage": "API key required"}`, `"Invalid API key"`, or a validation error like `"text: Expected string"`.

| HTTP status | errorCode | Scenario |
| --- | --- | --- |
| **400** | `40001` | Missing or invalid `text` field. |
| **401** | `40101` | Missing, invalid, expired, or revoked API key. |
| **413** | `41301` | Request body exceeds the maximum size limit. |
| **500** | `50001` | Internal error or upstream Tashkil service failure. |

## Go further

Diacritized text is the perfect input for speech.

- [Synthesize speech](/text-to-speech/synthesize) — Feed the diacritized text straight into TTS for precise pronunciation. — `POST /text-to-speech`

- [Audio narrative](/text-to-speech/audio-narrative) — Turn the polished text into an embeddable audio player. — `dashboard`

- [Translation](/understanding/translation) — Translate content between languages with streaming output. — `POST /translation/stream`

- [Error handling](/errors) — The shared errorCode / errorMessage format, in full. — `guide`


---

# Models

> Munsit offers state-of-the-art Arabic voice synthesis models designed to handle various dialects and use cases. List them with one call, then pass the model_id to any text-to-speech endpoint.

## Meet the model

**Faseeh** is Munsit's Arabic voice synthesis model — natural, high-quality speech across multiple Arabic dialects. Pass its `model_id` to any text-to-speech endpoint.

| Model | `model_id` | Highlights |
| --- | --- | --- |
| **Faseeh**  
High-quality Arabic voice synthesis model | `faseeh-v1-preview` | Natural-sounding Arabic speech · Multiple Arabic dialects supported · High-quality voice generation · Optimized for clarity and naturalness |

## List models

`GET /api/v1/models`

Retrieve a list of all available **text-to-speech** models. This endpoint does not return speech-to-text models — there is no discovery endpoint for those. The ASR models are `munsit` (default) and `munsit-en-ar`, passed as the `model` field on [transcription requests](/speech-to-text/transcribe).

Retrieve a list of all available voice synthesis models. Requires API key authentication via the `x-api-key` header. Returns an array of model objects.

```bash
curl -X GET "https://api.munsit.com/api/v1/models" \
  -H "x-api-key: YOUR_API_KEY"
```

```python
import requests

url = "https://api.munsit.com/api/v1/models"
headers = {
    "x-api-key": "YOUR_API_KEY"
}

response = requests.get(url, headers=headers)
models = response.json()
print(models)
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/models', {
  method: 'GET',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
  },
});

const models = await response.json();
console.log(models);
```

```go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    url := "https://api.munsit.com/api/v1/models"
    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    var models []map[string]any
    json.NewDecoder(resp.Body).Decode(&models)
    fmt.Println(models)
}
```

Response

```
[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "model_id": "faseeh-v1-preview",
    "model_name": "Faseeh",
    "description": "High-quality Arabic voice synthesis supporting multiple dialects"
  }
]
```

Response

```
[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "model_id": "faseeh-v1-preview",
    "model_name": "Faseeh",
    "description": "High-quality Arabic voice synthesis supporting multiple dialects"
  }
]
```

Response

```
[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "model_id": "faseeh-v1-preview",
    "model_name": "Faseeh",
    "description": "High-quality Arabic voice synthesis supporting multiple dialects"
  }
]
```

Response

```
[
  {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "model_id": "faseeh-v1-preview",
    "model_name": "Faseeh",
    "description": "High-quality Arabic voice synthesis supporting multiple dialects"
  }
]
```

## Response fields

Each model object contains:

| Field | Type | Description |
| --- | --- | --- |
| `model_id` | string | Model identifier used in API calls |
| `model_name` | string | Human-readable model name |
| `description` | string | null | Detailed description of the model |

## Usage

Use the `model_id` from the response in text-to-speech generation endpoints: POST `/text-to-speech/:model_id` (HTTP endpoint) or WS `/text-to-speech` (WebSocket endpoint — include `model_id` in the initConnection message).

> Caching. Model information doesn't change frequently. Consider caching the model list to reduce API calls.

## Go further

- [Synthesize](/text-to-speech/synthesize) — Use a model_id to generate a complete WAV file. — `POST /text-to-speech/{model_id}`

- [Audio streaming output](/text-to-speech/audio-streaming-output) — Pair Faseeh with streaming for real-time apps. — `streaming: true`

- [Voices](/text-to-speech/voices) — Pick a voice to go with your model. — `GET /voices`

- [WebSocket protocol](/reference/websocket) — The initConnection message and streaming lifecycle. — `WSS`


---

# Speech to Text

> Munsit converts spoken Arabic into accurate, structured text — high-accuracy recognition across dialects and accents, with word-level timestamps, speaker diarization and meeting intelligence built on top.

## Your first transcription

One POST with a file. Munsit analyzes the recording, converts the Arabic speech to text, and returns the transcript with its duration and word-level timestamps.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@meeting.mp3"
```

```python
import requests, os

r = requests.post(
    "https://api.munsit.com/api/v1/audio/transcribe",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    files={"file": open("meeting.mp3", "rb")},
)
print(r.json()["data"]["transcription"])
```

```javascript
import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("meeting.mp3")]), "meeting.mp3");

const res = await fetch(
  "https://api.munsit.com/api/v1/audio/transcribe",
  { method: "POST",
    headers: { "x-api-key": process.env.MUNSIT_API_KEY },
    body: form }
);
const { data } = await res.json();
console.log(data.transcription);
```

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	f, _ := os.Open("meeting.mp3")
	fw, _ := w.CreateFormFile("file", "meeting.mp3")
	io.Copy(fw, f)
	w.Close()

	req, _ := http.NewRequest("POST",
		"https://api.munsit.com/api/v1/audio/transcribe", &buf)
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	data := out["data"].(map[string]any)
	fmt.Println(data["transcription"])
}
```

Response

```
{
  "statusCode": 200,
  "data": {
    "transcriptionId": "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47",
    "transcription": "لك كلما عمقت الآخرين أصبحت قزما...",
    "duration": 53.661375,
    "timestamps": [
      { "word": "الأشياء", "start": 0.24, "end": 0.31 }
    ]
  },
  "message": "Success"
}
```

> No dialect parameter. Munsit is optimized for Arabic speech recognition with strong coverage of dialect and accent variation — the same call handles all of it. For speakers who mix Arabic and English mid-sentence, switch to the munsit-en-ar model below.

## Choose a model

Two models. Every **batch** endpoint accepts an optional `model` parameter; if omitted, `munsit` is used. Live streaming takes the same `model` values; its `language` parameter is `ar`\-only in v1. Custom vocabulary (`hotwords`) is ignored on `munsit-en-ar`.

| Model | ID | Use it for | Default |
| --- | --- | --- | --- |
| **Munsit** | `munsit` | **Arabic.** Optimized for Arabic speech recognition, with strong performance across dialects and accents. | Yes |
| **Munsit En-Ar** | `munsit-en-ar` | **Mixed Arabic–English spoken content** with code-switching support — for speakers who naturally alternate between the two languages within the same conversation or utterance. | — |

## What you can send

Twelve audio formats, covering common output from different recording platforms — no transcoding step.

.mp3.wav.m4a.flac.ogg.opus.webm.aac.amr.wma.mp2.m4r

| Workflow | Duration limit | Notes |
| --- | --- | --- |
| **Audio transcription** | Under 60 minutes | Pre-recorded files, word-level timing. |
| **Minutes of meetings** | Under 30 minutes | Structured transcripts optimized for meeting use cases. |
| **Live streaming** | No limit | Unbounded while the connection stays active. Keep the session alive with `KeepAlive` during pauses; unbroken speech is force-segmented about every 60 seconds. |

> Longer recordings? For Audio Transcription or Minutes of Meetings, split the audio into shorter segments for best performance.

## Go further

Three core workflows plus real-time streaming.

- [Transcribe →](/speech-to-text/transcribe) — Convert pre-recorded audio files into text with word-level timing. Full parameter and response reference. — `POST /audio/transcribe`

- [Live streaming →](/speech-to-text/streaming) — Real-time transcription over a WebSocket, with interim results, turn detection, and per-turn sentiment and gender. — `WSS /api/v1/listen`

- [Diarization →](/speech-to-text/diarization) — Identify who spoke and get speaker-labeled transcript segments merged with timing. — `POST /audio/diarization/transcribe`

- [Minutes of meetings →](/speech-to-text/minutes-of-meetings) — Structured transcripts optimized for meeting records and downstream analysis. — `POST /minutes-of-meeting/transcribe`


---

# Transcribe

> Upload pre-recorded Arabic audio as multipart/form-data and receive a high-quality transcript with total duration and word-level timestamps. Optimized for asynchronous processing of interviews, meetings, media clips and customer calls.

## Endpoint

`POST /api/v1/audio/transcribe`

Authenticate with your API key in the `x-api-key` header. See [Authentication](/authentication).

| Header | Value |
| --- | --- |
| `x-api-key` | YOUR\_MUNSIT\_API\_KEY |

## Request

Send the body as `multipart/form-data`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | file | **Yes** | Audio file in a supported format — see [what you can send](/speech-to-text/get-started#send). |
| `model` | string | No | ASR model to use: `munsit` (default) or `munsit-en-ar` (mixed Arabic-English with code-switching). |
| `hotwords` | string | No | Comma-separated custom vocabulary (multi-word phrases allowed). Biases recognition toward names, brands and domain terms. **Not supported with `munsit-en-ar`.** |
| `return_confidence` | boolean | No | When `true`, each `timestamps` entry includes a `confidence` score (0–1). The response is otherwise unchanged. |
| `return_timestamps` | boolean | No | Defaults to `true` on `munsit`; set `false` to return an empty `timestamps` array. On `munsit-en-ar` timestamps are off unless you set this to `true`. |
| `return_turns` | boolean | No | When `true`, adds the `turns` array and tags each turn with smart-turn `is_complete` and `turn_probability`. These two fields appear **only** with this flag. |
| `return_gender` | boolean | No | When `true`, adds the `turns` array with a `gender` object (`label`, `score`) per turn, plus a whole-file rollup in `analysis`. |
| `return_sentiment` | boolean | No | When `true`, adds the `turns` array with a `sentiment` object (`label`, `score`) per turn, plus a whole-file rollup in `analysis`. |

## Custom vocabulary

Pass rare terms the recognizer is unlikely to know — customer names, product codes, brand words. In our benchmarks, biasing recovered rare terms from 0% to 77% recall with overall accuracy unchanged or slightly better. Short lists of 5–30 genuinely rare terms work best; very long lists dilute the effect.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@call.wav" \
  -F "hotwords=عبد القادر,أديب" \
  -F "return_confidence=true"
```

Response — timestamps entry

```
{ "word": "الأشياء", "start": 0.24, "end": 0.31, "confidence": 0.994 }
```

> hotwords is ignored when model=munsit-en-ar. The mixed Arabic-English model does not support custom vocabulary.
> Transcribe only. hotwords and return_confidence apply to POST /audio/transcribe — not to diarization or minutes of meetings. For live audio, streaming takes hotwords as a query parameter and always returns confidence.

## Per-turn analysis

Set `return_turns`, `return_gender` and/or `return_sentiment` to break the transcript into turns and annotate each one. Any of the three adds the `turns` array — the gender and sentiment flags imply it — and each flag contributes only its own fields. `analysis` holds the whole-file rollup and appears only with `return_gender` or `return_sentiment`; `return_turns` on its own does not produce it. Short single-speaker recordings typically come back as one turn.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@call.wav" \
  -F "return_turns=true" \
  -F "return_gender=true" \
  -F "return_sentiment=true"
```

Response — 200

```
{
  "statusCode": 200,
  "data": {
    "transcription": "أهلا كيف حالك... بخير شكرا",
    "duration": 6.2,
    "turns": [
      {
        "turn_id": 0,
        "start": 0.0,
        "end": 2.8,
        "text": "أهلا كيف حالك",
        "is_complete": true,
        "turn_probability": 0.94,
        "gender": { "label": "male", "score": 0.88 },
        "sentiment": { "label": "neutral", "score": 0.81 }
      },
      {
        "turn_id": 1,
        "start": 3.1,
        "end": 6.2,
        "text": "بخير شكرا",
        "is_complete": true,
        "turn_probability": 0.97,
        "gender": { "label": "female", "score": 0.91 },
        "sentiment": { "label": "positive", "score": 0.86 }
      }
    ],
    "analysis": {
      "turns": 2,
      "gender": { "dominant": "female", "by_duration_s": { "male": 2.8, "female": 3.1 } },
      "sentiment": { "dominant": "positive", "counts": { "neutral": 1, "positive": 1 } }
    }
  },
  "message": "Success"
}
```

> This sentiment is a quick per-utterance signal returned alongside the transcript. For deeper, LLM-based analysis of an existing transcription — emotions, trends, critical moments — use Sentiment analysis instead. For live audio, streaming emits Sentiment and Gender events per turn.
> GET /history/speech-to-text/:id also returns turns and analysis for transcriptions created with one of these flags set.

## Example request

How it works: upload audio, Munsit analyzes the recording and converts the Arabic speech into text, and you get back a transcript with duration and word-level timestamps.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@meeting.mp3" \
  -F "model=munsit"
```

```python
import requests, os

r = requests.post(
    "https://api.munsit.com/api/v1/audio/transcribe",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    files={"file": open("meeting.mp3", "rb")},
    data={"model": "munsit"},
)
out = r.json()["data"]
print(out["transcription"], out["duration"])
```

```javascript
import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("meeting.mp3")]), "meeting.mp3");
form.append("model", "munsit");

const res = await fetch(
  "https://api.munsit.com/api/v1/audio/transcribe",
  { method: "POST",
    headers: { "x-api-key": process.env.MUNSIT_API_KEY },
    body: form }
);
const { data } = await res.json();
console.log(data.transcription, data.duration);
```

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	f, _ := os.Open("meeting.mp3")
	fw, _ := w.CreateFormFile("file", "meeting.mp3")
	io.Copy(fw, f)
	w.WriteField("model", "munsit")
	w.Close()

	req, _ := http.NewRequest("POST",
		"https://api.munsit.com/api/v1/audio/transcribe", &buf)
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	data := out["data"].(map[string]any)
	fmt.Println(data["transcription"], data["duration"])
}
```

Response — 200

```
{
  "statusCode": 200,
  "data": {
    "transcriptionId": "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47",
    "transcription": "لك كلما عمقت الآخرين أصبحت قزما...",
    "duration": 53.661375,
    "timestamps": [
      { "word": "الأشياء", "start": 0.24, "end": 0.31 }
    ],
    "summary": "",
    "audioUrl": "<stored-audio-url>",
    "stats": {
      "fileName": "meeting.mp3",
      "fileSize": "1.42 MB",
      "mimeType": "audio/mpeg",
      "creditsConsumed": 7
    }
  },
  "message": "Success"
}
```

> Files must be under 60 minutes. For longer recordings, split the audio into shorter segments for best performance — or use Streaming, which has no duration limit.

## Response fields

The payload arrives under `data`, alongside `statusCode` and `message`.

| Field | Type | Description |
| --- | --- | --- |
| `transcriptionId` | string (UUID) | Transcription identifier. Pass it as the path parameter to [sentiment analysis](/understanding/sentiment-analysis) and [keyword extraction](/understanding/keyword-extraction). |
| `transcription` | string | Full transcript text. |
| `duration` | number | Audio duration in seconds. |
| `timestamps` | array of objects (`word`, `start`, `end`, plus `confidence` when `return_confidence=true`) | Word-level timestamps. |
| `turns` | array of objects (`turn_id`, `start`, `end`, `text`; plus `is_complete` / `turn_probability` with `return_turns`, `gender` with `return_gender`, `sentiment` with `return_sentiment`) | Present when any of the three flags is `true` — `return_gender` and `return_sentiment` imply it. Short single-speaker clips typically return one turn. |
| `analysis` | object (`turns` count; plus `gender` and `sentiment` when those flags are set) | Whole-file rollup. Present only with `return_gender` or `return_sentiment` — `return_turns` alone does not produce it. |
| `attributes` | object | Internal metadata blob persisted with the transcription; mirrors fields above under different names (and repeats the full `timestamps` array). Prefer the named fields — treat this as unstable. |
| `summary` | string | Always present. Empty string on plain transcription; carries the generated summary on [minutes of meetings](/speech-to-text/minutes-of-meetings). |
| `audioUrl` | string | Stored copy of the uploaded audio. See [Audio retention](#retention). |
| `stats` | object (`fileName`, `fileSize`, `mimeType`, `creditsConsumed`) | Upload metadata and the credits billed for this request. |

## Audio retention

Audio submitted to the hosted API is stored server-side. Every successful response carries an `audioUrl` pointing at that stored copy, and the transcript text is persisted alongside it so it can be retrieved later through the history endpoints.

| What | Detail |
| --- | --- |
| **What is stored** | The uploaded audio file and the resulting transcript, keyed to your account. |
| **Access** | The response returns an `audioUrl` for the stored object. Do not treat it as a secret or hard-code it — the exact form is not part of the API contract and may change. |
| **Removal** | Delete a transcription and its stored audio through the history endpoints, or contact [support](/support) for bulk removal. |
| **Avoiding retention** | If your deployment cannot retain audio at all, use [self-hosting](/deployment/self-hosting), where storage is under your control. |

## Go further

Do more with your transcript.

- [Streaming →](/speech-to-text/streaming) — Live transcripts over a WebSocket instead of file upload. — `WSS /api/v1/listen`

- [Diarization →](/speech-to-text/diarization) — Speaker-labeled segments merged with the transcript. — `POST /audio/diarization/transcribe`

- [Minutes of meetings →](/speech-to-text/minutes-of-meetings) — Structured transcripts for meeting records. — `POST /minutes-of-meeting/transcribe`

- [Sentiment analysis →](/understanding/sentiment-analysis) — Analyze the tone of what was transcribed. — `POST /sentiment-analysis`


---

# Live streaming

> Stream audio over a WebSocket and receive Arabic transcripts as people speak — interim results while a sentence is in progress, a stable final when it completes, and a turn-boundary event your application can act on. First partials arrive in about 0.7 seconds (median), with word timestamps, per-word confidence, and per-turn sentiment and speaker gender.

## Endpoint & authentication

`WSS /api/v1/listen`

```
wss://api.munsit.com/api/v1/listen?api_key=YOUR_MUNSIT_API_KEY&encoding=linear16&sample_rate=16000
```

At least one auth method is required. If several are supplied, the `api_key` query parameter wins.

| Method | Where | Notes |
| --- | --- | --- |
| `x-api-key` | Header | Preferred for server-side clients. |
| `api_key` | Query parameter | For browser WebSocket clients, which cannot set headers. `x-api-key` is also accepted as a query parameter. |

> Keep connection URLs out of your logs. With query-parameter auth the key is part of the URL, so it lands in any request log or analytics tool that records it.

Connections are rejected with close code `1008` when authentication fails, when the key's concurrent-session limit is reached, or when the wallet balance is below about 60 seconds of runway — the `Error` message before the close says which.

## Query parameters

All optional. Telephony sources typically use `encoding=mulaw&sample_rate=8000`; microphone capture typically uses `encoding=linear16&sample_rate=16000`.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `encoding` | string | `linear16` | Encoding of the binary frames you send: `linear16` (16-bit PCM), `mulaw`, or `alaw`. |
| `sample_rate` | integer | `8000` | Sample rate of your audio: `8000` or `16000`. |
| `channels` | integer | `1` | `1` (mono) or `2` (stereo, interleaved). With `2`, each channel is transcribed independently — ideal for two-leg call recordings. |
| `model` | string | `munsit` | ASR model for the session: `munsit` (Arabic) or `munsit-en-ar` (mixed Arabic-English code-switching). Routes the stream to the matching engine. Both models share the `munsit-2` recognizer generation, so this selects language coverage, not model vintage. |
| `language` | string | `ar` | Transcription language. `ar` is the only supported value in v1. |
| `interim_results` | boolean | `true` | Emit interim (partial) results while a turn is in progress. |
| `endpointing` | integer | `800` | Milliseconds of silence that end a turn. Range `100`–`5000`. Retunable mid-session with `Configure`. |
| `smart_turn` | boolean | `true` | Gate end-of-turn on a semantic turn-completion model in addition to silence. A turn always ends after 2× the `endpointing` silence regardless. |
| `hotwords` | string | — | Comma-separated custom vocabulary (multi-word phrases allowed). Up to 200 entries, each up to 40 characters; URL-encode the value. |
| `correlation_id` | string | — | Your identifier for this session (up to 128 characters), echoed in the opening `Metadata` event. |
| `metadata` | string | — | Base64-encoded JSON object (up to 2 KB) attached to the session. |

> Invalid connection parameters are fatal. You receive an Error with code 4002 and recoverable: false, then the connection closes with code 4002. An invalid mid-session Configure is recoverable instead — the session continues.

## Sending audio

Send raw audio as **binary WebSocket frames** in the encoding and sample rate you declared at connect time. No container headers — for `linear16`, frames are little-endian 16-bit PCM samples, interleaved when `channels=2`. Frame size is up to you; roughly 20–200 ms of audio per frame works well.

| Rule | Why |
| --- | --- |
| **Keep the connection alive** | 12 seconds with neither audio nor a `KeepAlive` message produces an `Error` and closes with code `1011`. |
| **Don't run ahead of real time** | Audio may be buffered at most 60 seconds ahead of real time; exceeding it closes with code `4008`. Live sources never hit this — pace your sends when streaming from a file. |

## Control messages

Controls are JSON **text frames**, sent on the same socket as your binary audio.

```
{ "type": "KeepAlive" }
```

```
{ "type": "Configure", "endpointing": 300 }
```

```
{ "type": "CloseStream" }
```

Effect

```
Resets the 12-second idle timer during send pauses — for example while the caller is on hold.
```

Effect

```
Retunes the endpointing silence window mid-session (100–5000 ms). Applies to all channels.
```

Effect

```
Finalizes any in-progress turn, sends the closing Metadata event with billing, then closes with code 1000.
```

**CloseStream and short utterances.** `CloseStream` finalizes the turn in progress. Every event for that turn — `Results` with `is_final: true`, then `UtteranceEnd`, `Gender` and `Sentiment` — is sent before the closing `Metadata`, which is always the last event before the `1000` close.

This holds for utterances shorter than your `endpointing` window: a 400 ms word closed out with `CloseStream` still produces a final `Results`. It does **not** hold for audio that never opened a turn — under roughly 200 ms of speech, or noise-only input, the voice-activity detector never confirms speech, so the session ends with `Metadata` (`turn_count: 0`) and no `Results` at all. Treat that as “no speech detected” rather than an error.

After sending `CloseStream`, keep reading until the server closes the socket. Closing it yourself first aborts finalization and loses both the final `Results` and the billing `Metadata`.

This is the `/listen` counterpart to the legacy `end_of_stream` → `finalized` flush; the stable-text flag is `is_final` rather than `isFinal`, and `min_buffer_seconds` has no equivalent here.

## Server events

Every server message is a JSON text frame with a `type` field. All events carry `session_id` — and `correlation_id` when you set one — so multiplexed clients can attribute events without tracking connections.

A single spoken turn produces this sequence:

```
SpeechStarted → Results (interim, refreshing) → Results (final) → UtteranceEnd → Gender → Sentiment
```

```
{
  "type": "Metadata",
  "session_id": "0d5b1c9e-3f6a-4b62-9d8e-2f1a7c3b5e90",
  "correlation_id": "call-8371",
  "model": "munsit-v2",
  "protocol_version": 1,
  "channels": 1,
  "sample_rate": 16000,
  "dropped_hotwords": [],
  "sentiment": "available",
  "gender": "available"
}
```

```
{ "type": "SpeechStarted", "channel": 0, "ts": 4.31 }
```

```
{
  "type": "Results",
  "channel": 0,
  "turn_id": 2,
  "transcript": "لا تتكلم هكذا",
  "words": [
    { "word": "لا", "start": 5.02, "end": 5.18, "confidence": 0.996 },
    { "word": "تتكلم", "start": 5.18, "end": 5.61, "confidence": 0.988 },
    { "word": "هكذا", "start": 5.61, "end": 5.97, "confidence": 0.991 }
  ],
  "is_final": true,
  "speech_final": true,
  "language": "ar",
  "confidence": 0.992
}
```

```
{ "type": "UtteranceEnd", "channel": 0, "turn_id": 2, "last_word_end": 5.97 }
```

```
{ "type": "Gender", "channel": 0, "turn_id": 2, "label": "female", "score": 0.996 }

{ "type": "Sentiment", "channel": 0, "turn_id": 2, "label": "negative", "score": 0.87 }
```

```
{
  "type": "Metadata",
  "session_id": "0d5b1c9e-3f6a-4b62-9d8e-2f1a7c3b5e90",
  "audio_seconds_billed": 124.6,
  "turn_count": 9
}
```

```
{ "type": "Error", "code": 4002, "message": "Configure.endpointing must be 100..5000 ms", "recoverable": true }
```

When

```
Once, immediately after a successful connection.
```

When

```
The voice-activity detector confirmed speech on a channel. ts is the audio timestamp in seconds.
```

When

```
Continuously while the speaker talks. words[] carries session-absolute timestamps and per-word confidence; utterance confidence is null on interims.
```

When

```
Immediately after the final Results of a completed turn — never after a forced split. Gender and Sentiment follow it.
```

When

```
After each final result, including forced splits. Gender first, then Sentiment.
```

When

```
After CloseStream, right before the connection closes. Reports the session billing total.
```

When

```
recoverable: false always precedes a close with the matching code. recoverable: true means the session continues.
```

## Turn detection

**Endpointing** is the silence the engine waits for before declaring a turn finished, and it's the main latency/accuracy dial you control. You can retune it live, mid-session, with a `Configure` message — drop to `300` when your agent asks a yes/no question, restore `800` for open-ended answers.

| Setting | Final transcript arrives | Trade-off |
| --- | --- | --- |
| `800` ms (default) | ~1.2 s after speech ends | Safest turn boundaries for conversational speech. |
| `500` ms | ~0.9 s (estimated) | Balanced; clears most mid-sentence hesitations. |
| `300` ms | ~0.8 s | Fastest — best for short commands. On hesitant, conversational speech it can split turns mid-thought, costing ~2 WER points in our benchmarks. |

**Smart turn detection** (`smart_turn`, on by default) runs a semantic end-of-turn model on every pause, so a caller who stops mid-sentence to think isn't cut off just because the silence threshold elapsed. A turn still always ends after 2× the endpointing silence. If the model is unavailable server-side, endpointing degrades gracefully to silence-only.

> is_final vs speech_final. Treat speech_final as your end-of-turn signal for agent logic, and is_final as “this text will not change”.speech_final: true — the speaker genuinely finished. This is your trigger to respond.is_final: true with speech_final: false — a forced split during long unbroken speech, about every 60 seconds. The text is stable but the speaker is still talking: don't respond, no UtteranceEnd fires, and transcription continues under the next turn_id.

Interim results (`is_final: false`) refresh continuously and **may revise earlier words** — render them as provisional text. `UtteranceEnd` fires only on genuine turn ends and is the cleanest single signal for “caller stopped talking”.

## Custom vocabulary

Pass rare terms — customer names, brands, product codes — in the `hotwords` query parameter, comma-separated and URL-encoded. Multi-word phrases are allowed.

```
&hotwords=%D8%B9%D8%A8%D8%AF%20%D8%A7%D9%84%D9%82%D8%A7%D8%AF%D8%B1%2C%D8%A3%D8%AF%D9%8A%D8%A8
```

Short lists of 5–30 genuinely rare terms work best; very long lists dilute the effect. Up to 200 entries, each up to 40 characters. Entries that can't be applied are skipped and reported in `dropped_hotwords` on the opening `Metadata` event — check it to confirm every entry landed.

## Sentiment & speaker gender

After each final result you receive two enrichment events, with no extra requests.

| Event | Labels | Notes |
| --- | --- | --- |
| `Gender` | `male` / `female` | Classified from the turn's audio — measured 99.2% accuracy on Arabic. |
| `Sentiment` | `positive` / `neutral` / `negative` | Derived from the turn's transcript. |

On a two-channel call this gives you live per-party gender and a running sentiment trajectory — useful for routing, analytics and supervisor alerts. For post-call analysis of an existing transcription, use [Sentiment analysis](/understanding/sentiment-analysis) instead.

## Session health & close codes

Four habits keep a session healthy: send `KeepAlive` during pauses, pace file streaming to real time, end with `CloseStream` so the billing event arrives, and handle the close code.

| Code | Meaning | Client handling |
| --- | --- | --- |
| `1000` | Normal close after `CloseStream` | Done — billing was reported in the closing `Metadata`. |
| `1008` | Policy rejection: auth failed, concurrent-session limit reached, or insufficient wallet balance | Check the preceding `Error`. Auth: fix the key. Session limit: retry after a session ends. Balance: top up the wallet. |
| `1011` | Internal error, or 12 s with no audio and no `KeepAlive` | Reconnect and resume; send `KeepAlive` during pauses. |
| `4002` | Invalid connection parameters | Fix the parameters and reconnect. |
| `4008` | Audio sent more than 60 s ahead of real time | Pace file streaming to real time. |

## Billing & limits

Usage is metered as **seconds of audio received × number of channels**, charged from your wallet in 60-second cycles during the session.

| Limit | Value |
| --- | --- |
| **Concurrent sessions** | 5 per API key by default — raised on request. Exceeding it closes the new connection with `1008`. |
| **Wallet runway to connect** | About 60 seconds. Running out mid-session closes the connection with `1008` after an `Error`. |
| **Idle timeout** | 12 seconds with neither audio nor `KeepAlive` (code `1011`). |
| **Session length** | Unlimited while the connection stays active. Unbroken speech is force-segmented about every 60 seconds so results keep flowing. |

The closing `Metadata` event reports the session total as `audio_seconds_billed`.

## Examples

Streaming a file, and capturing a microphone in the browser.

```python
import asyncio, json, wave
import websockets

async def main():
    wav = wave.open("audio_16k_mono.wav")
    pcm = wav.readframes(wav.getnframes())

    url = ("wss://api.munsit.com/api/v1/listen"
           "?api_key=YOUR_MUNSIT_API_KEY&encoding=linear16&sample_rate=16000&interim_results=true")

    try:
        async with websockets.connect(url) as ws:
            async def send_audio():
                chunk = 6400  # 200 ms of 16 kHz 16-bit mono
                for i in range(0, len(pcm), chunk):
                    await ws.send(pcm[i:i + chunk])
                    await asyncio.sleep(0.2)  # pace at real time (close code 4008)
                await ws.send(json.dumps({"type": "CloseStream"}))

            sender = asyncio.create_task(send_audio())
            async for message in ws:
                event = json.loads(message)
                if event["type"] == "Results" and event["is_final"]:
                    print("FINAL:", event["transcript"])
                elif event["type"] == "Error":
                    print("error:", event["code"], event["message"])
                elif event["type"] == "Metadata" and "audio_seconds_billed" in event:
                    print("billed seconds:", event["audio_seconds_billed"])
                    break
            await sender
    except websockets.exceptions.ConnectionClosed as e:
        # match e.code against the close-code table above
        print("connection closed:", e.code, e.reason)

asyncio.run(main())
```

```javascript
// Acquire the microphone BEFORE connecting — the permission prompt
// can take longer than the 12-second idle timeout.
async function startTranscription() {
  const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
  const ctx = new AudioContext({ sampleRate: 16000 });
  await ctx.resume(); // autoplay policies can leave the context suspended
  // Browsers may ignore the sampleRate hint — declare what you actually got:
  const sampleRate = ctx.sampleRate;

  const ws = new WebSocket(
    `wss://api.munsit.com/api/v1/listen?api_key=YOUR_MUNSIT_API_KEY&encoding=linear16&sample_rate=${sampleRate}`
  );
  ws.binaryType = "arraybuffer";

  ws.onmessage = (msg) => {
    const event = JSON.parse(msg.data);
    if (event.type === "Results") {
      render(event.transcript, event.is_final);   // interims refresh, finals are stable
    } else if (event.type === "UtteranceEnd") {
      onTurnComplete(event.turn_id);              // trigger your agent here
    } else if (event.type === "Metadata" && event.audio_seconds_billed !== undefined) {
      console.log("billed seconds:", event.audio_seconds_billed);
    }
  };

  // Production code should prefer an AudioWorklet — ScriptProcessorNode is
  // deprecated but shown here for brevity.
  const source = ctx.createMediaStreamSource(mic);
  const processor = ctx.createScriptProcessor(2048, 1, 1);
  processor.onaudioprocess = (e) => {
    const f32 = e.inputBuffer.getChannelData(0);
    const i16 = new Int16Array(f32.length);
    for (let i = 0; i < f32.length; i++) i16[i] = Math.max(-32768, Math.min(32767, f32[i] * 32767));
    if (ws.readyState === WebSocket.OPEN) ws.send(i16.buffer);
  };
  source.connect(processor);
  processor.connect(ctx.destination);

  // Send CloseStream so the billing Metadata arrives, then release the mic.
  const stop = () => {
    if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "CloseStream" }));
    processor.disconnect();
    source.disconnect();
    mic.getTracks().forEach((t) => t.stop());
    ctx.close();
  };
  ws.onclose = (e) => { console.log("closed:", e.code); stop(); };
  return stop;
}
```

Output

```
FINAL: "اجتماع الفريق يبدأ الساعة العاشرة"
billed seconds: 124.6
```

Note

```
The server accepts only 8000 or 16000 Hz. If the browser reports another rate — 48000 is common — resample in an AudioWorklet before sending.
```

## Legacy endpoint

> WS /websocket/speech-to-text is deprecated. The previous streaming endpoint — JSON audio_chunk frames and a cumulative transcript string — remains available for existing integrations but receives no new recognition features: no word timestamps, turn events, hotwords, confidence, sentiment or gender. New integrations should use WS /api/v1/listen. Its message reference is on the WebSocket protocol page.Lifecycle. This endpoint is frozen, not scheduled for removal. It stays available for existing integrations with no functional changes; any change to that would be announced on the Changelog.If you are still on it: send {"event":"end_of_stream"} before closing the socket and wait for the finalized event. Closing alone does not flush buffered audio, so short answers are otherwise lost. See Finalizing a stream.

## Go further

Batch alternatives, and the agent frameworks that wrap this socket for you.

- [Transcribe →](/speech-to-text/transcribe) — Pre-recorded files with word-level timestamps, one POST. — `POST /audio/transcribe`

- [Diarization →](/speech-to-text/diarization) — Who said what — speaker-labeled segments. — `POST /audio/diarization/transcribe`

- [LiveKit →](/integrations/livekit) — Drop-in streaming STT for voice agents. — `plugin`

- [WebSocket protocol →](/reference/websocket) — The TTS socket, and the deprecated STT one. — `reference`


---

# Diarization

> Identify and label the speakers in multi-speaker Arabic audio, then get each speaker segment aligned with transcribed text and timestamps — who said what in meetings, interviews, podcasts and conversations.

## Endpoint

`POST /api/v1/audio/diarization/transcribe`

Authenticate with your API key in the `x-api-key` header. See [Authentication](/authentication).

| Header | Value |
| --- | --- |
| `x-api-key` | YOUR\_MUNSIT\_API\_KEY |

## Request

Send the body as `multipart/form-data`. How it works: you upload a multi-speaker file, Munsit identifies speaker turns and assigns speaker labels, and you receive transcription, diarization segments, and merged speaker-labeled text with timing.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | file | **Yes** | Audio file in a supported format — see [what you can send](/speech-to-text/get-started#send). |
| `model` | string | No | ASR model to use: `munsit` (default) or `munsit-en-ar` (mixed Arabic-English with code-switching). |

> Transcribe-only options. hotwords and return_confidence apply to POST /audio/transcribe only.

## Example request

Works like [Transcribe](/speech-to-text/transcribe), with speaker labels added to the response.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/diarization/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@interview.mp3" \
  -F "model=munsit"
```

```python
import requests, os

r = requests.post(
    "https://api.munsit.com/api/v1/audio/diarization/transcribe",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    files={"file": open("interview.mp3", "rb")},
    data={"model": "munsit"},
)
for seg in r.json()["data"]["merged"]:
    print(seg["speaker"], seg["text"])
```

```javascript
import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("interview.mp3")]), "interview.mp3");
form.append("model", "munsit");

const res = await fetch(
  "https://api.munsit.com/api/v1/audio/diarization/transcribe",
  { method: "POST",
    headers: { "x-api-key": process.env.MUNSIT_API_KEY },
    body: form }
);
const { data } = await res.json();
for (const seg of data.merged) console.log(seg.speaker, seg.text);
```

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	f, _ := os.Open("interview.mp3")
	fw, _ := w.CreateFormFile("file", "interview.mp3")
	io.Copy(fw, f)
	w.WriteField("model", "munsit")
	w.Close()

	req, _ := http.NewRequest("POST",
		"https://api.munsit.com/api/v1/audio/diarization/transcribe", &buf)
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	data := out["data"].(map[string]any)
	for _, seg := range data["merged"].([]any) {
		s := seg.(map[string]any)
		fmt.Println(s["speaker"], s["text"])
	}
}
```

Response — 200

```
{
  "statusCode": 200,
  "data": {
    "transcription": {
      "transcription": "السلام عليكم ورحمة الله وبركاته. كيف حالك اليوم؟",
      "timestamps": [
        { "word": "السلام", "start": 0.0, "end": 1.2 }
      ]
    },
    "diarization": {
      "segments": [
        { "start": 0.0, "end": 8.5, "speaker": "SPEAKER_00" }
      ]
    },
    "merged": [
      {
        "start": 0.0,
        "end": 8.5,
        "speaker": "SPEAKER_00",
        "text": "السلام عليكم ورحمة الله وبركاته"
      }
    ],
    "duration": 53.661375
  },
  "message": "Success"
}
```

## Response fields

Three views of the same audio arrive under `data`: the raw transcript, the speaker segments, and the two merged together.

| Field | Type | Description |
| --- | --- | --- |
| `transcription` | object | Transcript and word-level timestamps. |
| `diarization` | object | Speaker segments with start/end labels. |
| `merged` | array of objects (`start`, `end`, `speaker`, `text`) | Combined diarization + transcript. |
| `duration` | number | Total duration in seconds. |
| `transcriptionId` | string (UUID) | Transcription identifier. Use it as the path parameter for [diarization sentiment analysis](/speech-to-text/diarization-sentiment). |
| `originalTranscript` | string | Raw verbatim transcript. |
| `attributes` | object | Internal metadata blob persisted with the transcription. Prefer the named fields — treat this as unstable. |
| `audioUrl` | string | Stored copy of the uploaded audio. See [Audio retention](/speech-to-text/transcribe#retention). |
| `stats` | object (`fileName`, `fileSize`, `mimeType`, `creditsConsumed`) | Upload metadata and the credits billed for this request. |

> Most apps only need merged. Each entry is one speaker turn — speaker, its text, and start/end timing — ready to render as a conversation.

## Go further

What to run on a diarized conversation next.

- [Diarization + sentiment →](/speech-to-text/diarization-sentiment) — Analyze per-speaker sentiment from a diarization record. — `POST /diarization/{diarizationId}/sentiment-analysis`

- [Transcribe →](/speech-to-text/transcribe) — Plain transcription when you don't need speaker labels. — `POST /audio/transcribe`

- [Minutes of meetings →](/speech-to-text/minutes-of-meetings) — Structured meeting transcripts for documentation. — `POST /minutes-of-meeting/transcribe`

- [Keyword extraction →](/understanding/keyword-extraction) — Pull the key topics out of the conversation. — `POST /keyword-extraction`


---

# Diarization + sentiment

> Analyze sentiment from a diarization output by its diarization ID — overall tone, per-speaker sentiment and trends across the conversation, at the analysis depth you choose.

## Endpoint

`POST /api/v1/diarization/{diarizationId}/sentiment-analysis`

Authenticate with your API key in the `x-api-key` header. See [Authentication](/authentication).

| Header | Value |
| --- | --- |
| `x-api-key` | YOUR\_MUNSIT\_API\_KEY |

> This is post-call analysis of an existing diarized transcription. For live calls with one speaker per audio channel, streaming with channels=2 gives per-speaker turns with per-turn Sentiment events in real time.

## Request

The diarization record is addressed in the path; the analysis depth goes in the JSON body.

| Path parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `diarizationId` | string (UUID) | **Yes** | Diarization record ID — from a previous [Diarization](/speech-to-text/diarization) run. |

| Body field | Type | Required | Description |
| --- | --- | --- | --- |
| `analysis_depth` | string | No | `light`, `standard`, or `deep`. |

## Example request

Run [Diarization](/speech-to-text/diarization) first, then pass its record ID here.

```bash
curl -X POST "https://api.munsit.com/api/v1/diarization/42/sentiment-analysis" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"analysis_depth": "standard"}'
```

```python
import requests, os

diarization_id = 42
r = requests.post(
    f"https://api.munsit.com/api/v1/diarization/{diarization_id}/sentiment-analysis",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    json={"analysis_depth": "standard"},
)
print(r.json())
```

```javascript
const diarizationId = 42;

const res = await fetch(
  `https://api.munsit.com/api/v1/diarization/${diarizationId}/sentiment-analysis`,
  { method: "POST",
    headers: {
      "x-api-key": process.env.MUNSIT_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ analysis_depth: "standard" }) }
);
console.log(await res.json());
```

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	diarizationID := 42
	body, _ := json.Marshal(map[string]string{"analysis_depth": "standard"})

	url := fmt.Sprintf(
		"https://api.munsit.com/api/v1/diarization/%d/sentiment-analysis",
		diarizationID)
	req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	fmt.Println(out)
}
```

## Response highlights

The analysis covers the whole conversation and each speaker in it.

| Field | What it tells you |
| --- | --- |
| `language` | Language detected in the analyzed conversation. |
| `overall_sentiment` | Sentiment of the conversation as a whole. |
| `speaker_sentiment` | Sentiment broken down per diarized speaker. |
| `sentiment_trends` | How sentiment evolves across the conversation. |
| `confidence_score` | Confidence of the analysis. |

## Go further

The rest of the understanding stack, and where the diarization ID comes from.

- [Diarization →](/speech-to-text/diarization) — Produce the diarization record this endpoint analyzes. — `POST /audio/diarization/transcribe`

- [Sentiment analysis →](/understanding/sentiment-analysis) — Sentiment on plain transcripts, without speakers. — `POST /sentiment-analysis`

- [Keyword extraction →](/understanding/keyword-extraction) — Key topics from the same conversation. — `POST /keyword-extraction`

- [Minutes of meetings →](/speech-to-text/minutes-of-meetings) — Structured transcripts for meeting records. — `POST /minutes-of-meeting/transcribe`


---

# Minutes of meetings

> Structured transcription for Arabic meeting recordings. Upload meeting audio and get clean, readable output with duration and word-level timestamps — consistent Arabic meeting records ready for documentation and follow-up actions.

## Endpoint

`POST /api/v1/minutes-of-meeting/transcribe`

Authenticate with your API key in the `x-api-key` header. See [Authentication](/authentication).

| Header | Value |
| --- | --- |
| `x-api-key` | YOUR\_MUNSIT\_API\_KEY |

Upload the recording.

```bash
curl -X POST "https://api.munsit.com/api/v1/minutes-of-meeting/transcribe" \
  -H "x-api-key: $MUNSIT_API_KEY" \
  -F "file=@standup.mp3" \
  -F "model=munsit"
```

```python
import requests, os

r = requests.post(
    "https://api.munsit.com/api/v1/minutes-of-meeting/transcribe",
    headers={"x-api-key": os.environ["MUNSIT_API_KEY"]},
    files={"file": open("standup.mp3", "rb")},
    data={"model": "munsit"},
)
out = r.json()["data"]
print(out["transcriptionId"], out["transcription"])
```

```javascript
import fs from "node:fs";

const form = new FormData();
form.append("file", new Blob([fs.readFileSync("standup.mp3")]), "standup.mp3");
form.append("model", "munsit");

const res = await fetch(
  "https://api.munsit.com/api/v1/minutes-of-meeting/transcribe",
  { method: "POST",
    headers: { "x-api-key": process.env.MUNSIT_API_KEY },
    body: form }
);
const { data } = await res.json();
console.log(data.transcriptionId, data.transcription);
```

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
)

func main() {
	var buf bytes.Buffer
	w := multipart.NewWriter(&buf)
	f, _ := os.Open("standup.mp3")
	fw, _ := w.CreateFormFile("file", "standup.mp3")
	io.Copy(fw, f)
	w.WriteField("model", "munsit")
	w.Close()

	req, _ := http.NewRequest("POST",
		"https://api.munsit.com/api/v1/minutes-of-meeting/transcribe", &buf)
	req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
	req.Header.Set("Content-Type", w.FormDataContentType())

	resp, _ := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	var out map[string]any
	json.NewDecoder(resp.Body).Decode(&out)
	data := out["data"].(map[string]any)
	fmt.Println(data["transcriptionId"], data["transcription"])
}
```

Response — 200

```
{
  "statusCode": 200,
  "data": {
    "transcriptionId": "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47",
    "transcription": "لك كلما عمقت الآخرين أصبحت قزما...",
    "duration": 53.661375,
    "timestamps": [
      { "word": "الأشياء", "start": 0.24, "end": 0.31 }
    ]
  },
  "message": "Success"
}
```

## Request

Send the body as `multipart/form-data`. Processing is asynchronous and optimized for pre-recorded meeting audio: upload the recording, Munsit analyzes the content and generates a structured transcript, and you receive organized output with timing for easy review.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | file | **Yes** | Meeting recording audio file — see [supported formats](/speech-to-text/get-started#send). |
| `model` | string | No | ASR model to use: `munsit` (default) or `munsit-en-ar` (mixed Arabic-English with code-switching). |

> 30-minute limit. Minutes of Meetings supports files shorter than 30 minutes. For longer recordings, split the audio into shorter segments for best performance.
> Transcribe-only options. hotwords and return_confidence apply to POST /audio/transcribe only.

## Response fields

The payload arrives under `data`, alongside `statusCode` and `message`.

| Field | Type | Description |
| --- | --- | --- |
| `transcriptionId` | string (UUID) | Meeting transcription ID. |
| `transcription` | string | Full transcript. |
| `duration` | number | Audio duration in seconds. |
| `timestamps` | array of objects (`word`, `start`, `end`) | Word-level timing. |
| `originalTranscript` | string | Raw verbatim transcript, before summarisation. |
| `summary` | string | Generated meeting summary. |
| `attributes` | object | Internal metadata blob persisted with the transcription. Prefer the named fields — treat this as unstable. |
| `audioUrl` | string | Stored copy of the uploaded audio. See [Audio retention](/speech-to-text/transcribe#retention). |
| `stats` | object (`fileName`, `fileSize`, `mimeType`, `creditsConsumed`) | Upload metadata and the credits billed for this request. |

## Go further

Downstream analysis and the other shapes of transcription.

- [Keyword extraction →](/understanding/keyword-extraction) — Pull agenda topics and action items out of the meeting. — `POST /keyword-extraction`

- [Diarization →](/speech-to-text/diarization) — Who said what — speaker-labeled meeting segments. — `POST /audio/diarization/transcribe`

- [Transcribe →](/speech-to-text/transcribe) — General-purpose transcription for files under 60 minutes. — `POST /audio/transcribe`

- [Sentiment analysis →](/understanding/sentiment-analysis) — How the meeting felt, not just what was said. — `POST /sentiment-analysis`


---

# Sentiment analysis

> Analyze tone, emotions, and sentiment trends from transcribed audio — with Arabic sentiment interpretation that's aware of cultural context. Transcribe first, then point this endpoint at the transcriptionId.

## What you get

Sentiment analysis processes already transcribed audio to extract emotional insights and sentiment patterns, at multiple depth levels.

| Insight | Detail |
| --- | --- |
| **Overall sentiment** | Sentiment and polarity analysis across the full content. |
| **Key emotions** | Detected emotions with confidence scoring. |
| **Speaker breakdowns** | Speaker-level sentiment for each participant. |
| **Temporal trends** | How sentiment shifts across the conversation. |

Typical use cases: call center quality and customer experience monitoring; public speech, media, and interview sentiment tracking; meeting intelligence and post-call emotional analysis.

> This is post-call analysis of an existing transcription. For real-time per-turn sentiment during a live call, use the Sentiment event on WS /api/v1/listen — no extra request needed. Note that confidence_score here is the confidence of the sentiment analysis itself, unrelated to ASR confidence.

## How it works

This endpoint runs **on top of an existing transcription** — it never touches raw audio itself.

| Step | What happens |
| --- | --- |
| **1 · Transcribe first** | Run the audio through [Audio Transcription](/speech-to-text/transcribe). |
| **2 · Use the transcription ID** | Send the returned `transcriptionId` to Sentiment Analysis. |
| **3 · Choose depth** | Pick `light`, `standard`, or `deep` depending on how detailed you want the output. |
| **4 · Review insights** | Receive emotional and sentiment analysis for the full content and individual speakers. |

> Want sentiment per speaker turn, in one call? Diarization + sentiment transcribes, splits speakers and scores sentiment in a single request.

## Endpoint

`POST /api/v1/audio/{transcriptionId}/sentiment-analysis`

Authenticated with the `x-api-key` header.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `transcriptionId` | path | string | **Yes** | ID from the Audio Transcription response. |
| `analysis_depth` | body | string | No | `light`, `standard`, or `deep`. |

## Analyze a transcription

Replace `805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47` with the `transcriptionId` your transcription call returned.

```bash
curl -X POST "https://api.munsit.com/api/v1/audio/805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47/sentiment-analysis" \
  -H "x-api-key: YOUR_MUNSIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "analysis_depth": "standard" }'
```

```python
import requests

transcription_id = "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47"  # from the Audio Transcription response

response = requests.post(
    f"https://api.munsit.com/api/v1/audio/{transcription_id}/sentiment-analysis",
    headers={"x-api-key": "YOUR_MUNSIT_API_KEY"},
    json={"analysis_depth": "standard"},
)

analysis = response.json()
print(analysis["data"]["overall_sentiment"])
```

```javascript
const transcriptionId = "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47"; // from the Audio Transcription response

const response = await fetch(
  `https://api.munsit.com/api/v1/audio/${transcriptionId}/sentiment-analysis`,
  {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_MUNSIT_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ analysis_depth: 'standard' }),
  }
);

const analysis = await response.json();
console.log(analysis.data.overall_sentiment);
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    transcriptionID := "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47" // from the Audio Transcription response
    url := fmt.Sprintf("https://api.munsit.com/api/v1/audio/%s/sentiment-analysis", transcriptionID)
    body, _ := json.Marshal(map[string]string{"analysis_depth": "standard"})

    req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var analysis map[string]any
    json.NewDecoder(resp.Body).Decode(&analysis)
    fmt.Println(analysis["data"].(map[string]any)["overall_sentiment"])
}
```

## Response

The response covers full-content sentiment plus per-speaker and per-moment detail. Depth controls how much of it is populated.

| Field | What it holds |
| --- | --- |
| `language` | Detected language of the analyzed transcript. |
| `overall_sentiment` | Overall sentiment and polarity for the full content. |
| `key_emotions` | Key emotions detected, with confidence scoring. |
| `speaker_sentiment` | Speaker-level sentiment breakdowns. |
| `sentiment_trends` | Temporal sentiment trends across the conversation. |
| `critical_moments` | Notable emotional moments in the conversation. |
| `confidence_score` | Confidence for the analysis as a whole. |
| `analysis_depth` | The depth level the analysis ran at. |
| `processing_metadata` | Metadata about the processing run. |

## Go further

Sentiment is one lens on a transcript — the others are a call away.

- [Transcribe audio](/speech-to-text/transcribe) — Produce the transcriptionId this endpoint needs. — `POST /speech-to-text`

- [Diarization + sentiment](/speech-to-text/diarization-sentiment) — Speakers and sentiment together, in one request. — `POST /stt-diarization`

- [Keyword extraction](/understanding/keyword-extraction) — Pull entities, terms and themes from the same transcript. — `POST …/keyword-extraction`

- [Translation](/understanding/translation) — Translate the transcript with streaming output. — `POST /translation/stream`


---

# Keyword extraction

> Extract the important terms, entities, and themes from meeting transcripts — key people, organizations, technical terms, topics, and important numbers, grouped and structured. Runs on top of Minutes of Meetings.

## What you get

Keyword extraction intelligently identifies the most important keywords and phrases in your transcribed Arabic content, giving you structured insight into the main themes discussed.

| Insight | Detail |
| --- | --- |
| **Categorized keywords** | Keywords grouped by semantic category — people, organizations, technical terms, topics, numbers. |
| **Speaker keywords** | Speaker-level keyword identification. |
| **Temporal trends** | How keywords rise and fall across the conversation. |
| **Topic analysis** | Topic insights and keyword statistics for deeper understanding. |

Typical use cases: meeting summaries and action-focused reporting; search indexing and content tagging; topic discovery across large audio archives.

## How it works

This endpoint runs on top of a **Minutes of Meetings** transcription — process the meeting first, then extract.

| Step | What happens |
| --- | --- |
| **1 · Generate Minutes of Meeting** | Process the meeting audio with [Minutes of Meetings](/speech-to-text/minutes-of-meetings). |
| **2 · Use the transcription ID** | Send the returned `transcriptionId` to Keyword Extraction. |
| **3 · Choose extraction depth** | Pick `basic`, `standard`, or `comprehensive`. |
| **4 · Review results** | Get structured keywords, topic insights, and trend analysis. |

## Endpoint

`POST /api/v1/minutes-of-meeting/{transcriptionId}/keyword-extraction`

Authenticated with the `x-api-key` header.

| Parameter | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `transcriptionId` | path | string | **Yes** | ID from the Minutes of Meetings transcription. |
| `extraction_depth` | body | string | No | `basic`, `standard`, or `comprehensive`. |

## Extract keywords

Replace `805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47` with the `transcriptionId` from your Minutes of Meetings run.

```bash
curl -X POST "https://api.munsit.com/api/v1/minutes-of-meeting/805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47/keyword-extraction" \
  -H "x-api-key: YOUR_MUNSIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "extraction_depth": "standard" }'
```

```python
import requests

transcription_id = "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47"  # from the Minutes of Meetings response

response = requests.post(
    f"https://api.munsit.com/api/v1/minutes-of-meeting/{transcription_id}/keyword-extraction",
    headers={"x-api-key": "YOUR_MUNSIT_API_KEY"},
    json={"extraction_depth": "standard"},
)

result = response.json()
print(result["data"]["keywords_by_category"])
```

```javascript
const transcriptionId = "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47"; // from the Minutes of Meetings response

const response = await fetch(
  `https://api.munsit.com/api/v1/minutes-of-meeting/${transcriptionId}/keyword-extraction`,
  {
    method: 'POST',
    headers: {
      'x-api-key': 'YOUR_MUNSIT_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ extraction_depth: 'standard' }),
  }
);

const result = await response.json();
console.log(result.data.keywords_by_category);
```

```go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    transcriptionID := "805059bf-7c3f-4a1e-9d2b-1f0c6ae83b47" // from the Minutes of Meetings response
    url := fmt.Sprintf("https://api.munsit.com/api/v1/minutes-of-meeting/%s/keyword-extraction", transcriptionID)
    body, _ := json.Marshal(map[string]string{"extraction_depth": "standard"})

    req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var result map[string]any
    json.NewDecoder(resp.Body).Decode(&result)
    fmt.Println(result["data"].(map[string]any)["keywords_by_category"])
}
```

## Response

Structured keywords plus the analysis around them. Extraction depth controls how much detail is populated.

| Field | What it holds |
| --- | --- |
| `keywords_by_category` | Keywords grouped by semantic category. |
| `speaker_keywords` | Keywords attributed to each speaker. |
| `keyword_trends` | Temporal keyword trends across the conversation. |
| `topic_analysis` | Topic insights derived from the keywords. |
| `keyword_statistics` | Statistics for deeper understanding of the content. |
| `language` | Detected language of the transcript. |
| `mixed_languages` | Whether the content mixes languages. |
| `extraction_depth` | The depth level the extraction ran at. |
| `processing_metadata` | Metadata about the processing run. |

## Go further

Keywords pair naturally with the rest of the understanding stack.

- [Minutes of meetings](/speech-to-text/minutes-of-meetings) — Produce the transcriptionId this endpoint needs. — `POST /minutes-of-meeting`

- [Sentiment analysis](/understanding/sentiment-analysis) — Tone, emotions and trends from the same content. — `POST …/sentiment-analysis`

- [Translation](/understanding/translation) — Translate transcripts with streaming output. — `POST /translation/stream`

- [Transcribe audio](/speech-to-text/transcribe) — Plain transcription, when you don't need meeting minutes. — `POST /speech-to-text`


---

# Translation

> Translate text between languages with LLM-powered translation and real-time streaming output. Send text in any language, name a target language, and read the translation as it's generated over Server-Sent Events.

## What you get

Translation streams its response through **Server-Sent Events (SSE)**, so you receive translated text while it is still being generated rather than waiting for the full result.

| Included | Typical use case |
| --- | --- |
| Real-time translation between languages | Live chat and customer support translation |
| Streaming output as content is generated | Real-time subtitles and multilingual captions |
| Source language auto-detection | Cross-language workflows in apps and internal tools |
| Flexible model and prompt configuration | Tuning translation behavior per product surface |

The flow is three steps: send the source text and target language, receive streaming chunks while the translation is generated, then wait for the `complete` event to get the final translation.

## Endpoint

`POST /api/v1/translation/stream`

Unlike most Munsit endpoints, this one authenticates with **either an API key or a JWT token** in the `Authorization: Bearer` header.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `text` | string | **Yes** | Input text to translate. |
| `target_language` | string | **Yes** | Full target language name, such as `Arabic`. |
| `source_language` | string | No | Source language, or `string` for auto-detection. |
| `model_name` | string | No | Translation model, default `qwen/qwen3-32b`. |
| `prompt_name` | string | No | Prompt template, default `quick-translate`. |
| `prompt_version` | string | No | Prompt version, default `1.2.0`. |

## Translate with streaming

Keep the connection open and read events until `complete` arrives.

```bash
curl -N -X POST "https://api.munsit.com/api/v1/translation/stream" \
  -H "Authorization: Bearer YOUR_MUNSIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to Munsit",
    "target_language": "Arabic"
  }'
```

```python
import requests

response = requests.post(
    "https://api.munsit.com/api/v1/translation/stream",
    headers={"Authorization": "Bearer YOUR_MUNSIT_API_KEY"},
    json={"text": "Welcome to Munsit", "target_language": "Arabic"},
    stream=True,
)

# read Server-Sent Events until the `complete` event arrives
for line in response.iter_lines(decode_unicode=True):
    if line:
        print(line)
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/translation/stream', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_MUNSIT_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ text: 'Welcome to Munsit', target_language: 'Arabic' }),
});

// read Server-Sent Events until the `complete` event arrives
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(decoder.decode(value));
}
```

```go
package main

import (
    "bufio"
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    body, _ := json.Marshal(map[string]string{
        "text":            "Welcome to Munsit",
        "target_language": "Arabic",
    })

    req, _ := http.NewRequest("POST", "https://api.munsit.com/api/v1/translation/stream", bytes.NewReader(body))
    req.Header.Set("Authorization", "Bearer "+os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    // read Server-Sent Events until the `complete` event arrives
    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if line != "" {
            fmt.Println(line)
        }
    }
}
```

## Stream events

Four event types arrive over the SSE connection, in order.

| Event | What it carries |
| --- | --- |
| `session_info` | Request and session metadata. |
| `start` | Translation start, with the detected source language. |
| `chunk` | Partial translation content — append as it arrives. |
| `complete` | Final translation output and metadata. |

> Keep the connection open. The response is streamed as Server-Sent Events — don't close until you've received a complete event, or you'll miss the final translation output.

## Go further

Translation slots into any transcript pipeline.

- [Transcribe audio](/speech-to-text/transcribe) — Produce Arabic transcripts to translate. — `POST /speech-to-text`

- [Sentiment analysis](/understanding/sentiment-analysis) — Score tone and emotion on the same transcript. — `POST …/sentiment-analysis`

- [Keyword extraction](/understanding/keyword-extraction) — Pull entities and themes from meeting transcripts. — `POST …/keyword-extraction`

- [Authentication](/authentication) — API keys, JWT tokens, and where each is accepted. — `guide`


---

# Voice isolation

> Separate clean speech from background noise. Upload once, track the job as it runs, and get back a URL to the denoised audio — ready for transcription, voice cloning, or anything downstream.

## What it does

Voice isolation is AI-powered noise removal that preserves natural speech characteristics. It accepts both audio and video files — video uploads have their audio track extracted automatically. Typical jobs:

| Use case | Why isolate first |
| --- | --- |
| **Pre-processing for transcription** | Clean audio before sending it to [speech-to-text](/speech-to-text/transcribe) for better accuracy. |
| **Voice cloning preparation** | Isolate clean speech for better [voice cloning](/text-to-speech/voice-cloning) results. |
| **Podcast production** | Remove background noise from podcast recordings. |
| **Call quality enhancement** | Improve audio quality in telephony applications. |

## How it works

Voice isolation is an asynchronous, job-based pipeline in three moves:

| Step | Call | What happens |
| --- | --- | --- |
| **1 · Submit the file** | POST `/denoise` | Send a `multipart/form-data` body with the `audio` field. The response returns a `jobId` and a `denoiseId`. |
| **2 · Track progress** | GET `/denoise/{denoiseId}/progress` | Open an SSE connection to receive `processing`, `done`, and `error` events in real time. See [Job status](/audio/denoise-progress). |
| **3 · Retrieve the result** | GET `/denoise` | The `done` event carries the `url` of the denoised audio. You can also list past jobs and their final URLs. |

## Limits & formats

One upload field, hard caps on size and duration.

| Limit | Value |
| --- | --- |
| **Maximum file size** | 200 MB |
| **Maximum duration** | 15 minutes |
| **Audio formats** | `WAV`, `MP3`, `M4A`, `FLAC`, `OGG` |
| **Video containers** | `mp4`, `mov`, `mkv`, `webm`, `avi`, `m4v` — treated as video; the audio track is extracted before denoising. All other extensions are treated as audio. |

## Submit a denoise job

Submit denoise jobPOST/denoise

Queues an audio (or video) file for voice isolation. The endpoint accepts the upload, persists the original, creates a pending record, and enqueues a background job. Authenticate with your API key in the `x-api-key` header. Content type is `multipart/form-data`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `audio` | File | **Yes** | Audio or video file to denoise (max 200 MB, max 15 minutes duration). |

```bash
curl -X POST "https://api.munsit.com/api/v1/denoise" \
  -H "x-api-key: YOUR_API_KEY" \
  -F "audio=@input_audio.wav"
```

```
import requests

url = "https://api.munsit.com/api/v1/denoise"
headers = {
    "x-api-key": "YOUR_API_KEY"
}

with open("input_audio.wav", "rb") as audio_file:
    files = {"audio": audio_file}
    response = requests.post(url, files=files, headers=headers)

if response.status_code == 200:
    data = response.json()
    print(f"Queued denoising: denoiseId={data['denoiseId']}")
else:
    print(f"Error: {response.status_code} - {response.text}")
```

```javascript
import fs from "node:fs";

const formData = new FormData();
const audioFile = new Blob([fs.readFileSync("input_audio.wav")]);
formData.append("audio", audioFile);

const response = await fetch('https://api.munsit.com/api/v1/denoise', {
  method: 'POST',
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    // Note: Content-Type is set automatically by FormData
  },
  body: formData,
});

if (response.ok) {
  const { jobId, denoiseId } = await response.json();
  console.log('Queued denoising', { jobId, denoiseId });
  // Next: subscribe to /denoise/{denoiseId}/progress for live updates
} else {
  const error = await response.json();
  console.error('Error:', error);
}
```

```go
package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func main() {
    file, _ := os.Open("input_audio.wav")
    defer file.Close()

    var buf bytes.Buffer
    writer := multipart.NewWriter(&buf)
    part, _ := writer.CreateFormFile("audio", "input_audio.wav")
    io.Copy(part, file)
    writer.Close()

    req, _ := http.NewRequest("POST", "https://api.munsit.com/api/v1/denoise", &buf)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Content-Type", writer.FormDataContentType())

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Queued denoising: %s\n", body)
}
```

Response · 200 OK

```
{
  "jobId": "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1",
  "denoiseId": "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `jobId` | string | Identifier of the queued background job. Mirrors `denoiseId`. |
| `denoiseId` | string (UUID) | Unique identifier of the denoising record. Use this with the progress and list endpoints. |

> Async workflow. This endpoint only enqueues the job. The denoised audio URL is delivered via the progress SSE endpoint (done event) or can be fetched from the list endpoint below once the record's status is success.

## List denoise history

List denoise historyGET/denoise

Returns the authenticated user's denoising records ordered by most recently created first. Use this to retrieve the final audio URL of completed jobs, monitor pending jobs, or paginate through historical results.

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `offset` | integer (≥ 0) | No | Number of records to skip from the start of the result set. |
| `limit` | integer (≥ 1) | No | Maximum number of records to return. Omit to return all records. |

```bash
curl -X GET "https://api.munsit.com/api/v1/denoise?offset=0&limit=20" \
  -H "x-api-key: YOUR_API_KEY"
```

```
import requests

url = "https://api.munsit.com/api/v1/denoise"
headers = {"x-api-key": "YOUR_API_KEY"}
params = {"offset": 0, "limit": 20}

response = requests.get(url, headers=headers, params=params)
for record in response.json():
    print(record["id"], record["status"], record["audio_url"])
```

```javascript
const response = await fetch('https://api.munsit.com/api/v1/denoise?offset=0&limit=20', {
  headers: {
    'x-api-key': 'YOUR_API_KEY',
  },
});

const records = await response.json();
records.forEach((r) => {
  console.log(r.id, r.status, r.audio_url);
});
```

```go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "os"
)

func main() {
    req, _ := http.NewRequest("GET", "https://api.munsit.com/api/v1/denoise?offset=0&limit=20", nil)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    var records []map[string]any
    json.NewDecoder(resp.Body).Decode(&records)
    for _, r := range records {
        fmt.Println(r["id"], r["status"], r["audio_url"])
    }
}
```

Response · 200 OK

```
[
  {
    "id": "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1",
    "user_id": "user_123",
    "transaction_id": "txn_abc",
    "audio_url": "https://cdn.munsit.com/denoising/user_123/5b1d2f7e_denoised.wav",
    "original_audio_url": "https://cdn.munsit.com/denoising/user_123/5b1d2f7e_original.wav",
    "file_name": "meeting.wav",
    "audio_duration": 312.4,
    "audio_size": 10485760,
    "audio_format": "wav",
    "source_type": "audio",
    "status": "success",
    "error": null,
    "created_at": "2026-05-12T10:14:02.000Z",
    "updated_at": "2026-05-12T10:14:55.000Z"
  }
]
```

| Field | Type | Description |
| --- | --- | --- |
| `id` | string (UUID) | Denoising record id (same value as `denoiseId` returned by `POST /denoise`). |
| `user_id` | string | Owner of the record. |
| `transaction_id` | string | null | Wallet transaction id associated with billing for this job, if any. |
| `audio_url` | string | null | URL of the denoised audio. `null` until the job reaches `success`. |
| `original_audio_url` | string | URL of the originally uploaded file. |
| `file_name` | string | Original file name as submitted by the client. |
| `audio_duration` | number | Duration of the source audio in seconds. |
| `audio_size` | number | Size of the source file in bytes. |
| `audio_format` | string | Lower-cased file extension of the source file (e.g. `wav`, `mp4`). |
| `source_type` | `"audio"` | `"video"` | Whether the upload was an audio file or a video container. |
| `status` | `"pending"` | `"success"` | `"failed"` | Current job status. |
| `error` | string | null | Error message if `status` is `failed`. |
| `created_at` | string (ISO 8601) | Creation timestamp. |
| `updated_at` | string (ISO 8601) | Last update timestamp. |

## Errors

Errors come back as JSON with an `errorCode` and a human-readable `errorMessage`.

| Status | Code | Message |
| --- | --- | --- |
| **400** Bad Request | `400xx` | `audio is required and must be a file` |
| **400** Bad Request | `400xx` | `File size exceeds the maximum limit of 200MB. File size: <n>MB` |
| **400** Bad Request | `400xx` | `Duration exceeds the maximum limit of 15 minutes. Duration: <n> minutes` |
| **400** Bad Request | `400xx` | `offset must be a non-negative number` · `limit must be a positive number` (list endpoint) |
| **401** Unauthorized | `40101` | `Invalid or missing API key` |
| **402** Payment Required | `40201` | `Insufficient wallet balance` |

## Next steps

The job is queued — now watch it finish, then put the clean audio to work.

- [Job status](/audio/denoise-progress) — Stream live progress events over SSE until the denoised URL arrives. — `GET /denoise/{denoiseId}/progress`

- [Transcribe](/speech-to-text/transcribe) — Send the clean audio to the most accurate Arabic ASR. — `POST /speech-to-text`

- [Voice cloning](/text-to-speech/voice-cloning) — Denoised samples make noticeably better clones. — `POST /voices/clone`

- [Errors](/errors) — The full error code catalogue across the API. — `guide`


---

# Job status

> Subscribe to live progress events for a denoising job over Server-Sent Events. The connection emits processing events as work advances and terminates with either a done event carrying the final audio URL, or an error event.

## The request

Stream denoise progressGET/denoise/{denoiseId}/progress

Authenticate with your API key in the `x-api-key` header, and ask for an event stream via the `Accept` header.

| Path parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `denoiseId` | string (UUID) | **Yes** | The `denoiseId` returned by [`POST /denoise`](/audio/voice-isolation). |

| Header | Value | Description |
| --- | --- | --- |
| `x-api-key` | your API key | Required for authentication. |
| `Accept` | `text/event-stream` | Required to negotiate the SSE response. |

## The event stream

On success the server answers **200 OK** with `Content-Type: text/event-stream`, `Cache-Control: no-cache` and `Connection: keep-alive`. Each message is delivered as a `data:` line whose payload is a JSON object. The connection automatically closes after a terminal event (`done` or `error`), or after **15 minutes** of inactivity. The `stage` field discriminates the event:

| Stage | Fields | Description |
| --- | --- | --- |
| `processing` | `pct` (number, 0–100), `message` (string, optional) | Incremental progress update. |
| `done` | `url` (string), `denoiseId` (string) | Terminal success — `url` is the denoised audio URL. |
| `error` | `message` (string) | Terminal failure — human-readable error message. |

Example streamwhat the wire looks like

```
data: {"stage":"processing","pct":15,"message":"Extracting audio"}

data: {"stage":"processing","pct":62,"message":"Running noise reduction"}

data: {"stage":"done","url":"https://cdn.munsit.com/denoising/user_123/5b1d2f7e_denoised.wav","denoiseId":"5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"}
```

> Reconnecting. When you connect to an already-completed job, the server immediately emits the cached final event and closes — clients never miss completion and there is no need to poll separately.

## Example usage

Use any HTTP client that can read a streamed body — no special SSE library required.

```bash
curl -N "https://api.munsit.com/api/v1/denoise/5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1/progress" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Accept: text/event-stream"
```

```
import httpx, json

denoise_id = "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"
url = f"https://api.munsit.com/api/v1/denoise/{denoise_id}/progress"
headers = {
    "x-api-key": "YOUR_API_KEY",
    "Accept": "text/event-stream",
}

with httpx.stream("GET", url, headers=headers, timeout=None) as r:
    for line in r.iter_lines():
        if not line or not line.startswith("data: "):
            continue
        event = json.loads(line[6:])
        if event["stage"] == "processing":
            print(f"Progress: {event['pct']}% — {event.get('message','')}")
        elif event["stage"] == "done":
            print("Denoised audio URL:", event["url"])
            break
        elif event["stage"] == "error":
            print("Failed:", event["message"])
            break
```

```javascript
const denoiseId = "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1";

const res = await fetch(`https://api.munsit.com/api/v1/denoise/${denoiseId}/progress`, {
  headers: {
    'x-api-key': 'YOUR_API_KEY',
    'Accept': 'text/event-stream',
  },
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const event = JSON.parse(line.slice(6));
    if (event.stage === "processing") {
      console.log(`Progress: ${event.pct}%`, event.message);
    } else if (event.stage === "done") {
      console.log("Denoised audio ready:", event.url);
      break;
    } else if (event.stage === "error") {
      console.error("Denoising failed:", event.message);
      break;
    }
  }
}
```

```go
package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "strings"
)

func main() {
    denoiseID := "5b1d2f7e-3f81-4c2a-9c5d-7a2e91b4c7a1"
    url := fmt.Sprintf("https://api.munsit.com/api/v1/denoise/%s/progress", denoiseID)

    req, _ := http.NewRequest("GET", url, nil)
    req.Header.Set("x-api-key", os.Getenv("MUNSIT_API_KEY"))
    req.Header.Set("Accept", "text/event-stream")

    resp, _ := http.DefaultClient.Do(req)
    defer resp.Body.Close()

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if !strings.HasPrefix(line, "data: ") {
            continue
        }
        var event map[string]any
        json.Unmarshal([]byte(line[6:]), &event)
        switch event["stage"] {
        case "processing":
            fmt.Printf("Progress: %v%% — %v\n", event["pct"], event["message"])
        case "done":
            fmt.Println("Denoised audio URL:", event["url"])
            return
        case "error":
            fmt.Println("Failed:", event["message"])
            return
        }
    }
}
```

Output

```
Progress: 15% Extracting audio
Progress: 62% Running noise reduction
Denoised audio ready: https://cdn.munsit.com/denoising/user_123/5b1d2f7e_denoised.wav
```

## Errors

Before the stream is established, failures come back as plain JSON — not as SSE.

| Status | Body |
| --- | --- |
| **404** Not Found | `{"message": "Denoising job not found"}` |
| **401** Unauthorized | `{"errorCode": "40101", "errorMessage": "Invalid or missing API key"}` |

## Next steps

Once the `done` event delivers the URL, the clean audio is yours.

- [Voice isolation](/audio/voice-isolation) — Submit jobs and list past results with their final URLs. — `POST /denoise · GET /denoise`

- [Transcribe](/speech-to-text/transcribe) — Feed the denoised file straight into speech-to-text. — `POST /speech-to-text`


---

# LiveKit

> The livekit-plugins-munsit package brings both halves of an Arabic voice agent to LiveKit Agents: munsit.STT for speech recognition — optimized for Arabic, with Arabic/English code-switching through the munsit-en-ar model — and munsit.TTS for natural Arabic speech, streamed so the agent starts talking on the first audio chunk.

## Install & authenticate

You need Python 3.10+, a [Munsit account](https://app.munsit.com/), and a LiveKit Cloud account or self-hosted LiveKit server. Generate a key at [Munsit — API Keys](https://app.munsit.com/en/api-keys), then install the plugin.

```
pip install livekit-plugins-munsit
```

```
git clone https://github.com/CNTXTFZCO0/livekit-plugins-munsit.git
cd livekit-plugins-munsit
pip install -e .
```

Both `munsit.STT` and `munsit.TTS` read `MUNSIT_API_KEY` automatically. Put it in a `.env.local` alongside your LiveKit and LLM credentials:

```
# Munsit — speech-to-text and text-to-speech
MUNSIT_API_KEY=your_MUNSIT_API_KEY_here

# LiveKit Configuration
LIVEKIT_URL=wss://your-livekit-server.com
LIVEKIT_API_KEY=your_livekit_api_key
LIVEKIT_API_SECRET=your_livekit_api_secret

# LLM Configuration (for the agent's brain)
OPENAI_API_KEY=your_openai_api_key
```

> Your API key is only shown once. Save it securely. Keep it in .env.local (never committed to Git), use environment variables in production, and rotate keys periodically.

## Quick start

Create `arabic_agent.py`. Munsit handles both ends of the loop, so the caller is transcribed and answered in the same Arabic register — no second vendor in the pipeline. The Silero thresholds below are tuned for real-world microphones, where post-echo-cancellation audio from the agent's own speaker can otherwise be misinterpreted as user speech.

```
"""
Arabic voice assistant — Munsit STT + Munsit TTS
"""
from dotenv import load_dotenv
from livekit import agents
from livekit.agents import Agent, AgentServer, AgentSession
from livekit.plugins import munsit, openai, silero

load_dotenv(".env.local")

class ArabicAssistant(Agent):
    """Arabic-speaking voice assistant"""

    def __init__(self) -> None:
        super().__init__(
            instructions="""أنت مساعد صوتي ذكي يتحدث العربية بطلاقة.
            مهمتك مساعدة المستخدمين بالإجابة على أسئلتهم بطريقة واضحة ومفيدة.
            كن ودوداً ومحترماً في تعاملك."""
        )

server = AgentServer()

@server.rtc_session()
async def my_agent(ctx: agents.JobContext):
    session = AgentSession(
        stt=munsit.STT(model="munsit-en-ar"),   # Arabic + English code-switch
        llm=openai.LLM(model="gpt-4o", temperature=0.7),
        tts=munsit.TTS(
            voice_id="ar-uae-male-1",          # Copy IDs from the Voice Library
            model="faseeh-v1-preview",
            stability=0.75,
            speed=1.0,
        ),
        vad=silero.VAD.load(activation_threshold=0.6, min_speech_duration=0.3),
    )

    await session.start(room=ctx.room, agent=ArabicAssistant())

    await session.generate_reply(
        instructions="رحب بالمستخدم باللغة العربية وقدم نفسك كمساعد ذكي جاهز للمساعدة."
    )

if __name__ == "__main__":
    agents.cli.run_app(server)
```

Run it in dev mode to start a local LiveKit server, launch the agent, and get a test URL to open in your browser — allow microphone access, speak Arabic, and the agent answers in a Munsit voice. Use `start` for deployment against your LiveKit Cloud or self-hosted server.

```
python arabic_agent.py dev
```

```
python arabic_agent.py start
```

> Tip: use console mode while developing — it runs locally with your microphone and prints transcripts to stdout, no LiveKit server required. Fastest debugging loop there is.

## Speech-to-text

`munsit.STT()` uses **streaming mode** by default: it keeps a live WebSocket to Munsit, emits interim transcripts while the caller speaks, and finalizes each turn with word-level timestamps the moment the server's turn detection fires — the right choice for voice agents. Pass `mode="batch"` to instead buffer each utterance and POST it as one request.

| Mode | When to use | Endpoint | Latency | Word timestamps | Interim events |
| --- | --- | --- | --- | --- | --- |
| `streaming` (default) | Voice agents and live captions: first partials in ~0.7 s (median), server-side turn detection. | `WS /api/v1/listen` | ~0.7 s median to first partial; final ~1.2 s after speech ends at the default endpointing | Yes, on final transcripts, plus an utterance-level confidence score | Yes, through `INTERIM_TRANSCRIPT` events |
| `batch` | Transcribing recorded audio; pipelines where one request per utterance is preferred. | `POST /api/v1/audio/transcribe` | VAD detection + upload + server processing (~1–2 s for short utterances) | Yes, populated on `SpeechData.words` | No |

```
from livekit.plugins import munsit

streaming_stt = munsit.STT()
batch_stt = munsit.STT(mode="batch")
```

> STT.recognize(audio_buffer) always uses the batch HTTP endpoint, even when mode="streaming" is configured.

Pick the model by input language — and pin it explicitly (`model="munsit"` or `model="munsit-en-ar"`) so a future plugin default change never affects your agent.

| Model | Use case |
| --- | --- |
| `munsit-en-ar` | Mixed Arabic-English speech with code-switching. **This is the plugin default.** |
| `munsit` | Pure Arabic speech recognition (fastest, and the only model that supports custom vocabulary). |

Every constructor parameter on `munsit.STT()`:

| Parameter | Default | Description |
| --- | --- | --- |
| `mode` | `streaming` | `streaming` for the live WebSocket or `batch` for HTTP transcription. |
| `model` | `munsit-en-ar` | `munsit-en-ar` (Arabic-English code-switching) or `munsit` (pure Arabic). Applies to both modes — streaming routes the session to the matching live engine. |
| `api_key` | env `MUNSIT_API_KEY` | Munsit API key. |
| `base_url` | Munsit production WebSocket URL | Override the streaming WebSocket URL. |
| `batch_base_url` | Munsit production HTTPS URL | Override the batch HTTP URL. |
| `auth_method` | `header` | Authentication style: `header`, `bearer`, or `query`. |
| `sample_rate` | `16000` | Input rate hint. Streaming negotiates 8000/16000 with the server and resamples anything else automatically (LiveKit tracks are typically 48000). |
| `num_channels` | `1` | Number of audio channels. |
| `interim_results` | `True` | Emits interim transcripts in streaming mode. |
| `endpointing_ms` | `800` | Server-side silence window that ends a turn (100–5000). Retunable mid-session via `ListenStream.configure_endpointing()`. |
| `smart_turn` | `True` | Gate end-of-turn on the server's semantic turn-completion model in addition to silence. |
| `hotwords` | `None` | Custom vocabulary: rare terms or phrases (≤200 entries, ≤40 chars each). Works in both modes on `munsit`; ignored, with a warning, on `munsit-en-ar`. |
| `correlation_id` | `None` | Session identifier echoed on every streaming event (≤128 chars). |
| `return_confidence` | `False` | Batch only: include per-word confidence in `timestamps`. |
| `on_enrichment` | `None` | Callback receiving raw per-turn `Sentiment` / `Gender` event dicts from streaming. |
| `language` | `None` | Label attached to `SpeechData.language`; defaults to `ar`. |
| `http_session` | `None` | Custom `aiohttp.ClientSession`. |
| `extra_query_params` | `None` | Extra query params for the streaming WebSocket endpoint. |

Munsit accepts the API key in three places — all three work on both the batch HTTP endpoint and the streaming WebSocket. Pick whichever fits your deployment:

```
# Default: sends the key as the x-api-key header.
munsit.STT(auth_method="header")

# Authorization: Bearer <key>
munsit.STT(auth_method="bearer")

# Query parameter (?token=<key>): useful when an upstream proxy strips headers.
munsit.STT(auth_method="query")
```

> The 0.3.x streaming parameters are deprecated. endpointing="server_diff"/"client_vad", finalize_after_silence_ms, energy_filter and vad_silence_ms are superseded by server-side turn detection on /api/v1/listen. Each is still accepted with a DeprecationWarning, and finalize_after_silence_ms maps onto endpointing_ms.

## Text-to-speech

Everything tunable on `munsit.TTS()`. Streaming is always on — the agent starts speaking as soon as the first audio chunk is ready. Pick a voice from the [Voice Library](/text-to-speech/voices): listen, then click **"Copy Voice ID"** next to the one you want.

| Parameter | Values | What it does |
| --- | --- | --- |
| `voice_id` | e.g. `ar-uae-male-1` | The Arabic voice to synthesize with. Copy IDs from the [Voice Library](/text-to-speech/voices). |
| `model` | `faseeh-v1-preview` | High-quality Arabic voice synthesis. |
| `stability` | `0.0` – `1.0` | Voice consistency. **0.0–0.4** more expressive, creative, but can hallucinate · **0.5–0.7** balanced (recommended) · **0.8–1.0** very consistent, less variation. |
| `speed` | `0.7` – `1.2` | Speech rate. **0.7–0.9** slower (clearer for complex content) · **1.0** normal (default) · **1.1–1.2** faster (quick responses). |
| `sample_rate` | `8000` – `48000` | Output PCM rate requested from the API. Defaults to `48000` — the engine's native rate, so audio skips downsampling and needs no resampling for WebRTC. Lower it only for a narrowband transport such as telephony. |
| `dialect` | e.g. `emirati` | Optional pronunciation hint for the selected voice. |

Rules of thumb for `stability`: chatbots **0.6–0.8**, professional applications **0.8–1.0**, creative content **0.3–0.5**. You can also change voice settings at runtime with `update_options`:

```
# Start with one voice
tts = munsit.TTS(voice_id="ar-uae-male-1", speed=1.0)

# Switch to a different voice based on context
tts.update_options(
    voice_id="ar-hijazi-female-2",
    stability=0.8,
    speed=1.0
)
```

## Endpointing & recognize()

In streaming mode the **server** decides when a turn ends: it waits for `endpointing_ms` of silence, and with `smart_turn` enabled a semantic turn-completion model must also agree the speaker is finished — so a caller who pauses mid-thought isn't cut off. A turn always ends after 2× the silence window regardless. Both are tunable at construction, and the silence window can be retuned mid-session.

```
# Snappier finals: shorter silence window, semantic gating still on.
stt = munsit.STT(
    mode="streaming",
    endpointing_ms=300,
    smart_turn=True,
)

# Retune mid-session on the live stream (100–5000 ms).
stream = stt.stream()
await stream.configure_endpointing(800)
```

```
from livekit import rtc
from livekit.plugins import munsit

stt = munsit.STT()
frames = [...]  # list of rtc.AudioFrame
combined = rtc.combine_audio_frames(frames)

result = await stt.recognize(combined)
print(result.alternatives[0].text)
for word in result.alternatives[0].words:
    print(f"{word.start_time:.2f}s -> {word.end_time:.2f}s  {word}")
```

Use `recognize` directly when you have a recorded audio buffer — a voicemail or uploaded file — and don't need a live stream. It always hits the batch HTTP endpoint regardless of `mode` and returns a transcript with word-level timestamps.

## Tracking turn metrics

Each conversation turn carries timing data on its `ChatMessage`. Subscribe to `conversation_item_added` to read transcription delay, end-of-turn delay, and downstream LLM/TTS metrics. All values are reported in seconds.

```
from livekit.agents import ChatMessage

@session.on("conversation_item_added")
def on_item(event):
    msg = event.item
    if not isinstance(msg, ChatMessage):
        return

    metrics = msg.metrics or {}
    if msg.role == "user":
        transcription_delay = metrics.get("transcription_delay")
        end_of_turn_delay = metrics.get("end_of_turn_delay")
        if transcription_delay is not None:
            print(f"STT delay: {transcription_delay * 1000:.0f} ms")
        if end_of_turn_delay is not None:
            print(f"EOU delay: {end_of_turn_delay * 1000:.0f} ms")
    elif msg.role == "assistant":
        llm_ttft = metrics.get("llm_node_ttft")
        tts_ttfb = metrics.get("tts_node_ttfb")
        if llm_ttft:
            print(f"LLM TTFT: {llm_ttft * 1000:.0f} ms")
        if tts_ttfb:
            print(f"TTS TTFB: {tts_ttfb * 1000:.0f} ms")
```

> The previous metrics_collected event is deprecated. New integrations should use conversation_item_added and read metrics from ChatMessage.metrics.

## Troubleshooting

The failure modes we see most, and the fix for each.

| Symptom | Fix |
| --- | --- |
| **"Invalid API Key"** | Verify `MUNSIT_API_KEY` is available to the process running your agent: `echo $MUNSIT_API_KEY`. |
| **"Payment Required"** | Your Munsit account balance is low. Top up at [app.munsit.com](https://app.munsit.com/). |
| **"Rate Limit Exceeded"** | Too many requests. Implement rate limiting or contact [Munsit support](/support) to raise your limits. |
| **No final transcript** | Batch mode finalizes after LiveKit signals end-of-speech — make sure your `AgentSession` includes VAD: `silero.VAD.load(activation_threshold=0.6, min_speech_duration=0.3)`. |
| **Microphone feedback loop** | The agent transcribes its own TTS playback: residual audio leaks through the mic after echo cancellation, and `munsit-en-ar` is more sensitive to low-energy input than `munsit`. Tighten the VAD as above; if it persists, switch temporarily to `model="munsit"` to confirm it's model-specific, or run with headphones. |
| **Need live captions** | Use `munsit.STT(mode="streaming", interim_results=True)` when your UI needs transcript updates before the speaker finishes. |
| **No audio output** | Check: API key is valid, network is stable, microphone permissions granted, browser supports WebRTC, firewall allows WebSocket connections. |
| **Poor audio quality** | Raise `stability` to 0.8+, and check your network and audio codec support. |

> Support: livekit-plugins-munsit on PyPI · GitHub Issues · LiveKit Community · Munsit support. Apache 2.0 licensed.

## Where next

Go under the hood, or wire Munsit into a different agent framework.

- [STT streaming API](/speech-to-text/streaming) — The WebSocket that streaming mode rides on. — `WSS /api/v1/listen`

- [Batch transcription API](/speech-to-text/transcribe) — The HTTP endpoint behind batch mode and recognize(). — `POST /api/v1/audio/transcribe`

- [Browse voices](/text-to-speech/voices) — Listen to every Arabic voice and copy its ID. — `GET /voices`

- [Pipecat plugin](/integrations/pipecat) — The same Munsit STT and TTS in a different agent framework. — `pipecat-plugins-munsit`


---

# Pipecat

> Pipecat is an open-source Python framework for real-time voice and multimodal conversational agents. The pipecat-plugins-munsit package bridges Munsit TTS into Pipecat pipelines, so you can build Arabic voice agents with low-latency streaming audio.

## Install the plugin

You need Python 3.9+ and a [Munsit account](https://app.munsit.com/). Generate a key at [Munsit — API Keys](https://app.munsit.com/en/api-keys) before you start.

```
pip install pipecat-plugins-munsit
```

> Your API key is only shown once. Save it securely — set it as the MUNSIT_API_KEY environment variable rather than hardcoding it.

## Quick start

The service takes your API key and a shared `aiohttp` session. That's the whole handshake — drop the resulting `tts` processor into any Pipecat pipeline.

```
import aiohttp
from pipecat_plugins_faseeh import FaseehTTSService

async with aiohttp.ClientSession() as session:
    tts = FaseehTTSService(
        api_key="your-api-key",
        aiohttp_session=session,
    )
```

## Where it sits in the pipeline

In a typical Pipecat pipeline, data flows through processors in sequence:

```
Microphone → Transport → Munsit STT → LLM → Munsit TTS → Transport → Speaker
```

The user speaks; the transport (e.g. Daily WebRTC) captures audio; **Munsit STT** converts speech to text; the LLM (e.g. OpenAI GPT-4o) generates a response; **Munsit TTS** converts it to Arabic audio — streaming PCM16 at 24 kHz — and the transport plays it back. Pipecat's `TTSService` base class aggregates LLM tokens into complete sentences before calling Munsit: each sentence triggers one HTTP streaming request, and PCM16 chunks are yielded back to the pipeline as they arrive.

## Full example

A complete Daily-transported agent: auto-creates a Daily room, wires Munsit STT → GPT-4o → Munsit TTS, and speaks first when a participant joins.

```
import asyncio
import os

import aiohttp
from dotenv import load_dotenv

from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import EndFrame, LLMMessagesUpdateFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineParams, PipelineTask
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
    LLMContextAggregatorPair,
    LLMUserAggregatorParams,
)
from pipecat_plugins_munsit import MunsitSTTService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.daily.transport import DailyParams, DailyTransport

from pipecat_plugins_faseeh import FaseehTTSService

load_dotenv(override=True)

async def main():
    async with aiohttp.ClientSession() as session:
        # Auto-create a Daily room
        from pipecat.transports.daily.utils import DailyRESTHelper, DailyRoomParams

        daily_helper = DailyRESTHelper(
            daily_api_key=os.getenv("DAILY_API_KEY", ""),
            aiohttp_session=session,
        )
        room = await daily_helper.create_room(DailyRoomParams())
        token = await daily_helper.get_token(room.url)

        transport = DailyTransport(
            room.url,
            token,
            "Munsit Bot",
            DailyParams(
                audio_in_enabled=True,
                audio_out_enabled=True,
                audio_out_sample_rate=48000,
            ),
        )

        stt = MunsitSTTService(api_key=os.getenv("MUNSIT_API_KEY", ""))
        llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY", ""), model="gpt-4o")
        tts = FaseehTTSService(
            api_key=os.getenv("MUNSIT_API_KEY"),
            aiohttp_session=session,
        )

        messages = [
            {
                "role": "system",
                "content": "You are a helpful Arabic-speaking assistant. Respond in Arabic.",
            }
        ]
        context = LLMContext(messages=messages)
        context_aggregator = LLMContextAggregatorPair(
            context,
            user_params=LLMUserAggregatorParams(vad_analyzer=SileroVADAnalyzer()),
        )

        pipeline = Pipeline([
            transport.input(),
            stt,
            context_aggregator.user(),
            llm,
            tts,
            transport.output(),
            context_aggregator.assistant(),
        ])

        task = PipelineTask(pipeline, params=PipelineParams(allow_interruptions=True))

        @transport.event_handler("on_first_participant_joined")
        async def on_joined(transport, participant):
            await task.queue_frames([LLMMessagesUpdateFrame(messages, run_llm=True)])

        @transport.event_handler("on_participant_left")
        async def on_left(transport, participant, reason):
            await task.queue_frame(EndFrame())

        runner = PipelineRunner()
        await runner.run(task)

if __name__ == "__main__":
    asyncio.run(main())
```

## Configuration

Constructor parameters on `FaseehTTSService`. To pick a voice, browse the [Voice Library](/text-to-speech/voices), listen, and click **"Copy Voice ID"** next to the one you want.

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `api_key` | str | env `MUNSIT_API_KEY` | Munsit API key |
| `voice_id` | str | `ar-hijazi-female-2` | Voice identifier |
| `model` | str | `faseeh-v1-preview` | TTS model |
| `stability` | float | `0.5` | Voice consistency (0.0–1.0) |
| `speed` | float | `1.0` | Speech rate (0.7–1.2) |
| `sample_rate` | int | `48000` | Output PCM rate requested from the API. `48000` is the engine's native rate — no downsampling, no client-side resampling. Match your transport's `audio_out_sample_rate` to it. |
| `base_url` | str | `https://api.munsit.com/api/v1` | API base URL |

You can also change voice, model, speed, or stability mid-conversation by queueing a `TTSUpdateSettingsFrame`:

```
tts = FaseehTTSService(
    api_key="your-api-key",
    voice_id="ar-uae-male-1",  # Paste the copied voice ID here
    aiohttp_session=session,
)
```

```
from pipecat.frames.frames import TTSUpdateSettingsFrame

# Switch voice
await task.queue_frame(TTSUpdateSettingsFrame(settings={"voice_id": "ar-emirati-male-1"}))

# Adjust speed and stability
await task.queue_frame(TTSUpdateSettingsFrame(settings={"speed": 1.1, "stability": 0.8}))

# Switch model
await task.queue_frame(TTSUpdateSettingsFrame(settings={"model": "faseeh-v2"}))
```

## Error handling & troubleshooting

The plugin yields non-fatal `ErrorFrame` objects instead of raising exceptions — your pipeline keeps running even if a single TTS request fails.

| HTTP status | Meaning | Action |
| --- | --- | --- |
| `401` | Invalid API key | Check `MUNSIT_API_KEY` |
| `402` | Insufficient balance | Add credits at [app.munsit.com](https://app.munsit.com/) |
| `404` | Voice or model not found | Check `voice_id` and `model` |
| `429` | Rate limit exceeded | Reduce request frequency |

```
from pipecat.frames.frames import ErrorFrame

@task.event_handler("on_error")
async def on_error(task, error: ErrorFrame):
    logger.error(f"TTS error: {error.error}")
```

**No audio output?** Verify `MUNSIT_API_KEY` is set and valid, that `voice_id` exists in the voice library, and that the pipeline sample rate matches the plugin's (48000 Hz default). **High latency?** The plugin already uses HTTP streaming for lowest latency — check connectivity to `api.munsit.com`, or try a voice with faster generation characteristics. **Import errors?** Make sure both `pipecat-ai` and `pipecat-plugins-munsit` are installed; minimum Pipecat version is **0.0.100**.

## Where next

Explore the voices, or compare frameworks.

- [Browse voices](/text-to-speech/voices) — Every Arabic voice, with a preview and a copyable ID. — `GET /voices`

- [TTS API](/text-to-speech/synthesize) — The synthesis endpoint the plugin streams from. — `POST /text-to-speech`

- [LiveKit](/integrations/livekit) — The same Munsit STT and TTS in LiveKit Agents. — `livekit-plugins-munsit`

- [Errors](/errors) — The full error catalogue behind those status codes. — `guide`


---

# VAPI

> Plug Munsit's Arabic text-to-speech into VAPI voice assistants as a custom voice provider — natural Arabic on phone calls, with VAPI's real-time performance intact. You need a Munsit account and a VAPI account with access to custom voice configuration.

## How it works

VAPI's custom TTS system operates through a webhook pattern. There's no SDK to install — VAPI calls a Munsit endpoint directly, four steps per utterance:

| Step | What happens |
| --- | --- |
| **1 · Text conversion trigger** | During a conversation, VAPI needs to convert text to speech. |
| **2 · Request to Munsit** | VAPI sends a POST request to Munsit's TTS endpoint with text and audio specifications. |
| **3 · Audio generation** | Munsit generates Arabic audio and returns it as raw PCM data. |
| **4 · Real-time playback** | VAPI streams the audio to the caller in real time. |

## Set up authentication

VAPI stores your Munsit key as a **Custom Credential** and attaches it to every TTS request. First, create a key in the [Munsit dashboard](https://app.munsit.com/en/api-keys): click **"Generate New API Key"**, name it descriptively (e.g. "VAPI Integration"), and copy it immediately.

> Your API key will only be displayed once. Copy it securely before closing the dialog. Store keys in VAPI Custom Credentials — never hardcoded in assistant configuration — use separate credentials for dev and production, and rotate periodically.

Then create a Server Configuration at [VAPI Custom Credentials](https://dashboard.vapi.ai/settings/integrations/custom-credential) → **"Create New Server Configuration"**, with these exact settings:

| Field | Value |
| --- | --- |
| **Name** | A descriptive name, e.g. "Munsit TTS" |
| **Authentication Type** | Bearer Token |
| **Token** | Your Munsit API key |
| **Include Bearer** | **Disable** this toggle (turn it OFF) |
| **Header Name** | `x-api-key` |

Save, then copy the configuration's **Credential ID** — a UUID like `d4a3a362-fe82-4255-b475-f30eefe8e75c`, shown next to the configuration name (or in the URL when viewing its details). You'll reference it from your assistant configuration.

## The endpoint URL

The Munsit TTS endpoint URL encodes the model, voice, and tuning in its path and query string:

```
https://api.munsit.com/api/v1/integrations/vapi/{model_id}/{voice_id}?similarity={similarity}&speed={speed}
```

| Parameter | Type | Description | Example |
| --- | --- | --- | --- |
| `model_id` | string | The Munsit model to use — `faseeh-v1-preview` (Faseeh), high-quality Arabic voice synthesis. | `faseeh-v1-preview` |
| `voice_id` | string | The Arabic voice identifier. Browse and verify IDs in the [Voice Library](/text-to-speech/voices) — they're case-sensitive. | `ar-najdi-male-2` |
| `similarity` | number | Voice similarity/stability, 0.0–1.0 (typically 0.5–0.9). **0.0–0.4** more expressive · **0.5–0.7** balanced (recommended) · **0.8–1.0** very consistent. Rules of thumb: customer service 0.7–0.8, professional 0.8–0.9, creative 0.5–0.6. | `0.7` |
| `speed` | number | Speech speed multiplier, 0.7–1.2 (default 1.0). Below 1.0 slows down, above 1.0 speeds up. | `1.0` |

```
// Full model with Najdi male voice
"url": "https://api.munsit.com/api/v1/integrations/vapi/faseeh-v1-preview/ar-najdi-male-2?similarity=0.7&speed=1.0"

// Faseeh with UAE female voice
"url": "https://api.munsit.com/api/v1/integrations/vapi/faseeh-v1-preview/ar-uae-female-1?similarity=0.8&speed=1.0"

// Full model with Hijazi male voice (slower speech)
"url": "https://api.munsit.com/api/v1/integrations/vapi/faseeh-v1-preview/ar-hijazi-male-1?similarity=0.75&speed=0.9"
```

## Configure your assistant

Point the assistant's voice at your Munsit URL, reference the Credential ID from step 2, and always configure a fallback voice provider so calls continue if the endpoint has issues. Replace the example Credential ID with your own.

```
{
  "name": "Munsit Assistant 2",
  "voice": {
    "provider": "custom-voice",
    "server": {
      "url": "https://api.munsit.com/api/v1/integrations/vapi/faseeh-v1-preview/ar-najdi-male-2?similarity=0.7&speed=1.0",
      "credentialId": "d4a3a362-fe82-4255-b475-f30eefe8e75c",
      "timeoutSeconds": 30
    },
    "fallbackPlan": {
      "voices": [
        {
          "provider": "eleven-labs",
          "voiceId": "21m00Tcm4TlvDq8ikWAM"
        }
      ]
    }
  }
}
```

## On the wire

Every TTS request from VAPI arrives as a `voice-request` message; Munsit answers with raw streamed PCM. You don't have to build either side — this is what flows between them.

```
{
  "message": {
    "type": "voice-request",
    "text": "مرحباً، كيف يمكنني مساعدتك اليوم؟",
    "sampleRate": 24000,
    "timestamp": 1677123456789,
    "call": {
      "id": "call-123",
      "orgId": "org-456"
    },
    "assistant": {
      "id": "assistant-789",
      "name": "Munsit Assistant"
    },
    "customer": {
      "number": "+1234567890"
    }
  }
}
```

```
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Transfer-Encoding: chunked

[Raw PCM audio bytes]
```

Required request fields: `type` (always `"voice-request"`), `text` (supports Arabic and mixed content), `sampleRate` (8000, 16000, 22050, or 24000 Hz), and `timestamp` (Unix milliseconds). The audio Munsit returns is raw PCM — no headers or containers — mono, 16-bit signed integer, little-endian, at the requested sample rate.

> Munsit handles all audio format requirements automatically. No additional configuration needed.

## Test the integration

Use VAPI's API to create a test call that exercises your Munsit TTS assistant end to end.

```
async function testFaseehWithVAPICall() {
  const vapiApiKey = 'your-vapi-api-key';
  const assistantId = 'your-assistant-id'; // Assistant with Munsit TTS

  const callData = {
    assistant: { id: assistantId },
    phoneNumberId: 'your-phone-number-id',
    customer: { number: '+1234567890' }, // Your test number
  };

  try {
    const response = await fetch('https://api.vapi.ai/call', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${vapiApiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(callData),
    });

    const call = await response.json();
    console.log('Test call created:', call.id);
    return call;
  } catch (error) {
    console.error('Failed to create test call:', error);
  }
}
```

## Troubleshooting

Symptoms, likely causes, and what to change.

| Symptom | Common causes | Solutions |
| --- | --- | --- |
| **Request timeouts** — VAPI doesn't receive audio, calls may drop | Network issues between VAPI and the Munsit API; TTS takes longer than the timeout; server overload | Increase `timeoutSeconds` (default 30); check network and Munsit API status |
| **Audio playback problems** — no audio, or distorted/garbled sound | Incorrect URL format or parameters; invalid voice ID; authentication issues | Verify the URL matches `/vapi/{model_id}/{voice_id}?similarity={similarity}`; confirm the voice ID exists; check the credential ID holds a valid Munsit key |
| **Authentication failures** — 401 Unauthorized | Invalid Munsit key in custom credentials; missing/incorrect credential ID; key expired or revoked | Verify the key at [app.munsit.com](https://app.munsit.com/); check the credential ID against the VAPI dashboard; regenerate if needed |
| **High latency** — noticeable delays in conversation | Network latency; high similarity values requiring more processing | Reduce similarity (e.g. 0.9 → 0.7); consider geographic proximity of services |
| **Invalid voice ID** — 404 Not Found | Typo; voice doesn't exist; wrong model/voice combination | Verify IDs in the [Voice Library](/text-to-speech/voices) — exact and case-sensitive — and check the voice is available for your model |

## Where next

Now that Munsit TTS is answering calls: try different voices, combine Munsit for Arabic with other providers for English, and watch usage in the [dashboard](https://app.munsit.com/).

- [Explore voices](/text-to-speech/voices) — Try different Arabic voices from the library. — `GET /voices`

- [TTS API](/text-to-speech/synthesize) — The synthesis engine behind the VAPI endpoint. — `POST /text-to-speech`

- [Ultravox](/integrations/ultravox) — The custom-voice pattern on Ultravox. — `guide`

- [Support](/support) — Schedule a meeting with the Munsit team — VAPI questions go to docs.vapi.ai. — `guide`


---

# Ultravox

> Ultravox is a voice orchestration platform that connects multiple text-to-speech providers through a unified interface. Using Custom Voices, Ultravox can call external TTS APIs such as Faseeh and use those voices seamlessly inside Ultravox workflows. All you need is an active Faseeh (Munsit) account and an active Ultravox account.

## Get your key and voice ID

Two things to collect from the Faseeh side before you touch Ultravox:

| Step | What to do |
| --- | --- |
| **1 · Create an API key** | Log in to your Faseeh account and navigate to the [API / Developer section](https://app.munsit.com/en/api-keys). Create a new API key and copy it — it will be used inside Ultravox. |
| **2 · Copy the Voice ID** | Go to the [Voices section](/text-to-speech/voices), select the voice you want, and use the button that copies the **Voice ID**. Save it for later. |

> Keep the API key secure. It goes into your Ultravox custom-voice configuration, so treat that configuration as a secret too.

## Add a Custom Voice in Ultravox

In the Ultravox dashboard: open **Voices** from the navigation, click **Custom Voice**, then **Add Custom Voice**. Fill in the two identity fields:

| Field | Best practice |
| --- | --- |
| **Voice Name** | Use the same name as the voice in Faseeh — it makes the voice easier to identify and manage later. |
| **Description** | A short description: language, dialect, gender, or intended use case. |

## Configure the Custom Voice request

Copy the configuration below into Ultravox. Replace the redacted API key with your Faseeh key, and `YOUR_ar-najdi-male-2` with the Voice ID you copied.

```
{
  "url": "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview",
  "headers": {
    "x-api-key": "REDACTED",
    "Content-Type": "application/json"
  },
  "body": {
    "text": "{text}",
    "stability": 0.5,
    "speed": 1.0,
    "streaming": true,
    "voice_id": "YOUR_ar-najdi-male-2"
  },
  "responseSampleRate": 24000
}
```

## Request details

**Authentication:** Munsit requires API key authentication using the `x-api-key` header. **Path parameter:** `model_id` (string, required) — the model identifier used for text-to-speech generation. The body fields:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `voice_id` | string | required | The Munsit voice ID used for synthesis. |
| `text` | string | required | The Arabic text to convert into speech — Ultravox substitutes it via the `{text}` placeholder. |
| `stability` | number | required | A value between 0.0 and 1.0. Higher values produce more consistent output. |
| `speed` | number | optional | A value between 0.7 and 1.2. Default is 1.0. |
| `streaming` | boolean | required | Must be `false` to receive a complete WAV file response. |

> Prefer watching it done? There's a step-by-step video walkthrough of this setup on YouTube.

## Where next

The Ultravox custom voice is a thin wrapper over the standard synthesis endpoint — everything about it applies here.

- [Synthesize API](/text-to-speech/synthesize) — The endpoint your Ultravox custom voice calls. — `POST /text-to-speech/{model_id}`

- [Browse voices](/text-to-speech/voices) — Preview voices and copy IDs for new custom voices. — `GET /voices`

- [VAPI](/integrations/vapi) — The custom-voice pattern on VAPI's webhook. — `guide`

- [Voice cloning](/text-to-speech/voice-cloning) — Clone a voice, then wire its ID into Ultravox. — `POST /voices/clone`


---

# All endpoints

> Every route in the Munsit API on one page — regional base URLs first, then the full endpoint index grouped by product area. Everything authenticates the same way: your key in the x-api-key header.

## Regional base URLs

Munsit provides regional API endpoints to ensure optimal performance and data residency compliance. Use the endpoint closest to your users or the one that matches your data residency requirements. All API functionality is identical across regions.

| Region | Base URL | Dashboard |
| --- | --- | --- |
| **Global (Default)** | `https://api.munsit.com/api/v1` | [app.munsit.com](https://app.munsit.com) |
| **UAE** | `https://ae.api.faseeh.ai/api/v1` | [ae.app.munsit.com](https://ae.app.munsit.com) |

> Keys are per region. Each regional endpoint has its own dashboard. To use the UAE endpoint, you must sign up and generate your API key from the UAE dashboard at ae.app.munsit.com. API keys are not interchangeable between regions.

> Data residency. If you have data residency requirements for the UAE, use the UAE regional endpoint (ae.api.faseeh.ai).

## How to use

Four steps, and any endpoint below works against either region:

| Step | Do this |
| --- | --- |
| **1 · Sign up on the correct dashboard** | Use [app.munsit.com](https://app.munsit.com) for the global endpoint or [ae.app.munsit.com](https://ae.app.munsit.com) for the UAE endpoint. |
| **2 · Choose your base URL** | Select the regional endpoint that matches your dashboard. |
| **3 · Include your API key** | Add your API key in the `x-api-key` header for authentication. |
| **4 · Set Content-Type** | Include `Content-Type: application/json` for JSON requests. |

## Text to Speech & voices

Synthesis, streaming audio output, the voice catalogue, cloning, and Arabic diacritization.

[POST/text-to-speech/{model\_id}→Generate speech from text with a chosen model.](/text-to-speech/synthesize) [WSS/websocket/text-to-speech→Stream audio out as text arrives.](/text-to-speech/audio-streaming-output) [POST/text-to-speech/{model\_id}/with-timestamps→Generate speech with character-level timings.](/text-to-speech/word-timestamps) [GET/voices→List available voices.](/text-to-speech/voices) [POST/voices/preview→Preview a voice.](/text-to-speech/voice-preview) [POST/voices/clone→Clone a voice from a sample.](/text-to-speech/voice-cloning) [POST/tashkil/diacritize→Diacritize Arabic text (tashkīl).](/text-to-speech/tashkil) [GET/models→List available models.](/text-to-speech/models)

## Speech to Text

File transcription, live streaming, speaker separation and meeting summaries.

[WSS/listen→Live Arabic transcription with turn detection, word timestamps, and per-turn sentiment and gender.](/speech-to-text/streaming) [POST/audio/transcribe→Transcribe an audio file with the munsit ASR model.](/speech-to-text/transcribe) [WSS/websocket/speech-to-text→Legacy real-time transcription — deprecated, use `/listen`.](/speech-to-text/streaming) [POST/audio/diarization/transcribe→Transcribe with speaker diarization.](/speech-to-text/diarization) [POST/diarization/{diarizationId}/sentiment-analysis→Sentiment analysis on a diarized transcript.](/speech-to-text/diarization-sentiment) [POST/minutes-of-meeting/transcribe→Transcribe and produce minutes of a meeting.](/speech-to-text/minutes-of-meetings)

## Understanding

Analysis on top of any transcript you've produced.

[POST/audio/{transcriptionId}/sentiment-analysis→Sentiment analysis on a transcription.](/understanding/sentiment-analysis) [POST/minutes-of-meeting/{transcriptionId}/keyword-extraction→Extract keywords from a meeting transcription.](/understanding/keyword-extraction) [POST/translation/stream→Stream a translation of a transcript.](/understanding/translation)

## Audio

Clean audio before you use it — asynchronous denoise jobs with live progress.

[POST/denoise→Submit a denoise (voice isolation) job.](/audio/voice-isolation) [GET/denoise→List denoise history and final audio URLs.](/audio/voice-isolation) [GET/denoise/{denoiseId}/progress→Stream job progress over SSE.](/audio/denoise-progress)

## Direct API calls

Only the base URL changes between regions.

```
# Base URL: https://api.munsit.com/api/v1

curl -X POST "https://api.munsit.com/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "streaming": true,
    "speed": 1
  }'
```

```
# Base URL: https://ae.api.faseeh.ai/api/v1

curl -X POST "https://ae.api.faseeh.ai/api/v1/text-to-speech/faseeh-v1-preview" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "voice_id": "ar-najdi-male-2",
    "text": "مرحبا بك في فصيح",
    "stability": 0.5,
    "streaming": true,
    "speed": 1
  }'
```


---

# WebSocket protocol

> Munsit runs two WebSockets: one that streams text in and returns audio chunks, and one that streams audio in and returns live transcripts. This page is the full wire protocol for both — connection, auth, message shapes and lifecycle.

## The two sockets

Same host, same `/api/v1` prefix, opposite directions.

| Socket | Path | You send | You receive |
| --- | --- | --- | --- |
| WSS **Text to speech** | `/websocket/text-to-speech` | JSON messages carrying text chunks | Base64-encoded PCM audio chunks |
| WSS **Speech to text** (legacy) | `/websocket/speech-to-text` | Audio chunks (WAV, then WAV or raw PCM) | Cumulative Arabic transcript strings |

> When to use the TTS socket. It shines when input text is streamed or generated in chunks and you need low-latency, real-time audio. If the entire text is available upfront, partial generation adds buffering — a standard HTTP request can be lower latency and is much simpler for quick prototyping.

## Text to speech — audio out

Streaming synthesisWSS/websocket/text-to-speech

```
wss://api.munsit.com/api/v1/websocket/text-to-speech?x-api-key=YOUR_API_KEY
```

**Authentication.** API key via the `x-api-key` query parameter, or in the initial connection message as `x_api_key`. After the socket opens you must send an `initConnection` message before any text.

```
{
  "type": "initConnection",
  "model_id": "faseeh-v1-preview",
  "voice_id": "ar-najdi-male-2",
  "voice_settings": {
    "stability": 0.5,
    "similarity_boost": 0.75,
    "speed": 1.0
  },
  "output_format": "pcm_24000",
  "x_api_key": "YOUR_API_KEY"
}
```

```
{
  "type": "text",
  "text": "مرحبا بك في فصيح ",
  "flush": false,
  "try_trigger_generation": false
}
```

```
{
  "type": "clear"
}

// Clears the current text buffer. No response message.
```

```
{
  "type": "closeConnection"
}

// Closes the WebSocket connection gracefully.
```

Response

```
{ "type": "connectionInitialized" }
```

Response

```
{ "audio": "base64_encoded_audio_data", "sampleRate": 24000 }
```

Response

```
// none
```

Response

```
// connection closes
```

**`initConnection` fields.**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | **Yes** | Must be `"initConnection"`. |
| `model_id` | string | No | Model ID to use (default: `"faseeh-v1-preview"`). |
| `voice_id` | string | **Yes** | The voice ID to use for synthesis. |
| `voice_settings` | object | No | Voice configuration. |
| `voice_settings.stability` | number | No | Stability setting (default: 0.5). |
| `voice_settings.similarity_boost` | number | No | Similarity boost (default: 0.75). |
| `voice_settings.speed` | number | No | Speed setting, range 0.7–1.2 (default: 1.0). |
| `output_format` | string | No | Audio output format: `"pcm_8000"`, `"pcm_16000"`, `"pcm_22050"`, `"pcm_24000"` (default: `"pcm_24000"`). This socket tops out at 24 kHz — for 48 kHz engine-native audio use the HTTP endpoints ([synthesize](/text-to-speech/synthesize), [streaming output](/text-to-speech/audio-streaming-output), [voice preview](/text-to-speech/voice-preview)) with `sample_rate=48000`. |
| `x_api_key` | string | No | API key (if not provided in query parameter). |

**`text` fields and the audio response.**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | **Yes** | Must be `"text"`. |
| `text` | string | **Yes** | Text to convert to speech. |
| `flush` | boolean | No | Force generation of audio even if buffer is small (default: `false`). |
| `try_trigger_generation` | boolean | No | Attempt to trigger generation immediately (default: `false`). |

| Response field | Type | Description |
| --- | --- | --- |
| `audio` | string | Base64-encoded PCM audio data. |
| `sampleRate` | number | Sample rate of the audio (typically 24000 Hz). |

## Speech to text — audio in

> The speech-to-text socket below is deprecated. WS /websocket/speech-to-text remains available for existing integrations but receives no new recognition features — no word timestamps, turn events, hotwords, confidence, sentiment or gender. New integrations should use WS /api/v1/listen. Finalization (end_of_stream) and min_buffer_seconds were added here as correctness fixes, not as new capability. It is frozen, not scheduled for removal, and stays available for existing integrations; any change to that would be announced on the Changelog.

Streaming transcriptionWSS/websocket/speech-to-text

```
wss://api.munsit.com/api/v1/websocket/speech-to-text?x-api-key=YOUR_MUNSIT_API_KEY&model=munsit
```

**Authentication.** The server accepts any one of three methods; at least one is required. If auth is invalid, the connection is rejected or closed — there is no dedicated `authentication_error` event.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `x-api-key` | string | No | API key in header or query. |
| `Authorization` | string | No | Bearer token header. |
| `token` | string | No | Token query param fallback for browsers. |

| Query parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | No | ASR model to use: `munsit` (default) or `munsit-en-ar` (mixed Arabic-English with code-switching). |
| `min_buffer_seconds` | number | No | Seconds of audio that must accumulate before an interim `transcription` is emitted. Default `0.5`, clamped to `0.1`–`5.0`. Lower it for faster partials on short answers. It does not gate the final result — the pass triggered by `end_of_stream` runs at any duration. |

**Audio input.** The first chunk must be WAV (with headers); subsequent chunks can be WAV or raw PCM. `audioBuffer` must be an array of byte values (0–255). Any chunk size works — 100–500 ms is typical for live audio, and smaller chunks lower latency without changing what is recognized. Two client message formats are accepted:

```
{
  "event": "audio_chunk",
  "data": {
    "audioBuffer": [1, 2, 3]
  }
}
```

```
{
  "audioBuffer": [1, 2, 3]
}
```

**Server events.**

| Event | Direction | Type | Meaning |
| --- | --- | --- | --- |
| `audio_chunk` | client → server | `Array<Uint8>` | Audio bytes; first chunk should include full WAV headers, PCM accepted after. |
| `end_of_stream` | client → server | — | Signals end of audio. Transcribes whatever is still buffered, at any duration, and replies with a final `transcription` then `finalized`. Send this before closing. |
| `transcription` | server → client | string | Cumulative Arabic transcript generated from all received chunks. Carries an `isFinal` boolean. |
| `finalized` | server → client | string | The complete transcript for the session. Emitted once, in response to `end_of_stream`. Safe to close the socket after this. |
| `transcription_error` | server → client | string | Error details during streaming transcription. |

**Finalizing a stream.** Closing the socket does **not** flush buffered audio, and waiting after the audio stops does not produce a final result — an interim hypothesis stays `isFinal: false` indefinitely, and audio shorter than `min_buffer_seconds` produces no event at all. Send `end_of_stream`, wait for `finalized`, then close.

```
{ "event": "end_of_stream" }
```

```
{ "event": "transcription", "data": "نعم", "isFinal": true }
{ "event": "finalized", "data": "نعم" }
```

> Single-word answers — «نعم», «لا», a city name — are typically under min_buffer_seconds, so end_of_stream is the only thing that will return them. Clients that open one socket per utterance must send it every time. On WS /api/v1/listen the equivalent is the CloseStream control frame, and the flag is is_final.

**Recommended flow.** Connect with auth → confirm the socket is open on the client side → emit `audio_chunk` payloads as audio arrives → listen for `transcription_error` and handle failures → listen for `transcription` and render live text updates → send `end_of_stream` when the speaker stops → wait for `finalized` → close the socket.

## Errors

On the TTS socket, errors arrive as a typed JSON message. On the STT socket, listen for the `transcription_error` event; invalid auth is handled by rejecting or closing the connection.

Error messagetext-to-speech socket

```
{
  "type": "error",
  "errorCode": 40101,
  "errorMessage": "Invalid API key"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `type` | string | Always `"error"`. |
| `errorCode` | number | Numeric error code (e.g. 40101, 40001). |
| `errorMessage` | string | Human-readable error message. |

## Full lifecycle example

Open, initialize, stream text, collect audio, handle errors — the complete TTS round-trip in the browser or Node.

```javascript
const ws = new WebSocket('wss://api.munsit.com/api/v1/websocket/text-to-speech?x-api-key=YOUR_API_KEY');

ws.onopen = () => {
  // Initialize connection
  ws.send(JSON.stringify({
    type: "initConnection",
    model_id: "faseeh-v1-preview",
    voice_id: "ar-najdi-male-2",
    voice_settings: {
      stability: 0.5,
      similarity_boost: 0.75,
      speed: 1.0
    },
    output_format: "pcm_24000"
  }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === "connectionInitialized") {
    // Connection ready, send text
    ws.send(JSON.stringify({
      type: "text",
      text: "مرحبا بك في فصيح "
    }));
  } else if (data.audio) {
    // Process audio chunk
    const audioData = atob(data.audio);
    // Handle audio playback
  } else if (data.type === "error" || data.errorCode) {
    console.error("Error:", data.errorMessage);
  }
};

ws.onerror = (error) => {
  console.error("WebSocket error:", error);
};

ws.onclose = () => {
  console.log("WebSocket closed");
};
```

## Best practices

Five habits that keep streams smooth.

| Practice | Why |
| --- | --- |
| **Always initialize** | Send `initConnection` immediately after opening the TTS connection. |
| **Handle errors** | Check for error messages in responses. |
| **Flush when done** | Use `flush: true` when sending the last text chunk to ensure all audio is generated. |
| **Close gracefully** | Send `closeConnection` before closing the WebSocket. |
| **Buffer audio** | Collect audio chunks and play them sequentially for smooth playback. |

- [TTS streaming guide](/text-to-speech/audio-streaming-output) — The task-oriented walkthrough for streamed synthesis. — `WSS /websocket/text-to-speech`

- [STT streaming guide](/speech-to-text/streaming) — The task-oriented walkthrough for live transcription. — `WSS /websocket/speech-to-text`


---

# Self hosting

> Run Munsit entirely within your own infrastructure — inside your data center or private environment. Built for enterprises and regulated industries that require full control over data, security, and performance, with native Arabic speech perfected for the GCC and MENA region and no compromise in English quality.

## Why on-premises?

By removing dependency on public networks, on-prem deployments deliver predictable latency, strong governance, and guaranteed data sovereignty.

| Benefit | What it means |
| --- | --- |
| **Ultra-low latency** | Local deployment eliminates internet round-trips, enabling **sub-100ms latency** for real-time voice and conversational workloads. |
| **Data residency & compliance** | All data remains inside your controlled environment — ideal for banking, healthcare, telecom, and government deployments. |
| **Full infrastructure control** | You control infrastructure sizing, scaling, networking, security policies, and upgrade cycles, aligned with internal IT and compliance standards. |

## Security

On-prem deployment ensures **zero data transmission over the public internet**. Built for environments where data control is non-negotiable:

| Guarantee | Detail |
| --- | --- |
| **Data stays inside** | Customer data stays fully within your network. |
| **Integrates with your stack** | Works with internal IAM, firewalls, and security tooling. |
| **Regulatory-ready** | Supports strict privacy, audit, and regulatory requirements. |

## Performance

Munsit on-prem is optimized for production-grade, real-time workloads:

| Characteristic | Detail |
| --- | --- |
| **Latency** | Sub-100ms median latency for short to mid-length utterances. |
| **Concurrency** | Stable, predictable performance under high concurrency. |
| **Workloads** | Optimized for conversational AI, voice agents, and real-time applications. |
| **Naturalness** | High naturalness in both Arabic (GCC-native dialects) and English. |

> Actual performance depends on deployment configuration, concurrency, and tuning.

## Models available

Munsit on-prem ships the production-ready Faseeh voice synthesis model:

| Model | Strengths | Ideal for |
| --- | --- | --- |
| **Faseeh** | Superior prosody and naturalness · best-in-class voice cloning and dubbing quality · fewer pronunciation and expression errors. | Media, dubbing, premium voice agents, conversational AI, call centers, and high-fidelity use cases. |

## Deployment support

CNTXT supports on-prem deployments across customer-owned data centers, private environments, and cloud providers using dedicated or isolated infrastructure. We assist with **hardware procurement, provisioning, and setup**, whether machines run on-prem or in supported cloud environments.

> Microsoft Partner. CNTXT is a Microsoft Partner, enabling aligned enterprise deployments with Microsoft ecosystem tooling, governance, and support where required.

## When to choose on-prem

Choose on-prem deployment if you require:

| Requirement |
| --- |
| Guaranteed data residency |
| Regulatory compliance |
| Predictable, ultra-low latency |
| Deep integration with internal systems |
| Long-term scalability under your control |

## Next steps

To proceed with an on-prem deployment: validate hardware and capacity requirements, align on network and security prerequisites, then contact the Munsit team for deployment architecture and sizing guidance. Enterprise support and tailored deployment options are available.

- [Reach out](mailto:support@munsit.com) — Deployment architecture and sizing guidance from the Munsit team. — `support@munsit.com`

- [Data privacy & compliance](/deployment/compliance) — HIPAA Mode, data ownership, and certifications. — `guide`


---

# Data privacy & compliance

> Munsit is built to meet the requirements of regulated, production-grade voice AI systems, where data privacy, security, and control are non-negotiable. Compliance is enforced at the architecture level — not through policy documents.

## HIPAA compliance

Munsit supports **HIPAA-compliant deployments** through an explicit **HIPAA Mode**. When HIPAA Mode is enabled:

| Guarantee | Detail |
| --- | --- |
| **No stored recordings** | No call recordings are stored on Munsit servers. |
| **In-memory processing** | Audio streams are processed in-memory only. |
| **Zero persistence** | Zero persistent storage of voice data. |
| **Customer ownership** | All data ownership remains with the customer. |
| **No secondary usage** | No secondary usage of audio for training or analytics. |

> Available everywhere. HIPAA Mode is available across SaaS, Dedicated VPC, and On-Prem deployments — so voice AI companies and enterprises can confidently use Munsit for healthcare, financial services, government, and regulated enterprise workloads without compromising compliance.

## Data ownership & control

Munsit follows a **customer-first data model**:

| Principle | Detail |
| --- | --- |
| **Full ownership** | Customers retain full ownership of all audio, transcripts, and metadata. |
| **Opt-in retention** | No recordings are retained unless explicitly configured by the customer. |
| **Deployment-level controls** | Deployment-level controls determine retention, logging, and access policies. |
| **Sovereign-ready** | Suitable for air-gapped and sovereign environments. |

## Security & certifications

Munsit is designed with enterprise security standards from day one.

| Standard | Status |
| --- | --- |
| **HIPAA** | Certified |
| **SOC 2** | _In progress_ |
| **ISO 27001** | _In progress_ |

Security controls already implemented include:

| Control |
| --- |
| Strict access isolation per tenant |
| Encrypted data in transit |
| Environment-level security boundaries (SaaS, VPC, On-Prem) |
| Operational auditability for enterprise customers |

## Built for regulated voice AI

Munsit is trusted in environments where voice data is sensitive by default, retention must be explicitly disabled, infrastructure must support **on-prem or sovereign hosting**, and compliance is enforced at the architecture level. It enables teams to build real-time voice AI systems with confidence — without sacrificing privacy, performance, or control.

- [Self hosting](/deployment/self-hosting) — Run Munsit inside your own data centre or private environment. — `on-prem guide`

- [Support](/support) — Talk to the team about compliant deployment options. — `guide`

## Customer responsibility

Munsit provides the underlying speech and inference infrastructure but does not control, moderate, or assume responsibility for customer-generated content. Customers are **solely responsible** for:

| Responsibility |
| --- |
| The content generated by applications using Munsit |
| Ensuring generated outputs comply with applicable laws, regulations, and industry standards |
| Implementing appropriate human review, guardrails, and usage policies where required |


---

# Changelog

> New endpoints, improvements, and API changes to Munsit — newest first.

August 14, 2026

## Word timestamps for Text to Speech

-   **New** New `POST /api/v1/text-to-speech/{model_id}/with-timestamps` returns speech together with character-level timings for the text you submitted. Use it to highlight words as they are spoken, drive captions, or align a transcript to the audio.
-   The response is NDJSON — one JSON object per line. Audio lines carry base64 PCM16 in `audio_base64`; the final lines carry `alignment` (aligned to your original text) and `normalized_alignment` (aligned to the engine's normalized form). Concatenating `characters` reproduces your input exactly, so array indices map straight back onto your string.
-   Break tags are supported in the text you send: `<break time="3s"/>` and `<break time="500ms"/>` insert a pause of the given length.
-   **API** Timings are emitted once generation finishes, not incrementally with each audio chunk. To highlight from the first word, buffer the full response before playback or request long text sentence by sentence.
-   Requires a model served by the v1.5 engine. Other models return `400` with `Word timestamps are not available for model '…'`. Pricing, wallet deduction, and history are identical to a standard synthesis request — timestamps cost nothing extra.

July 27, 2026

## Streaming finalization on the legacy STT socket

-   **New** Send `{"event":"end_of_stream"}` to transcribe any remaining buffered audio, at any duration. The server replies with a `transcription` carrying `isFinal: true`, then a `finalized` event with the complete transcript. Closing the socket does not flush buffered audio — send this first.
-   `transcription` events now carry an `isFinal` boolean. Clients reading only `data` are unaffected.
-   New `min_buffer_seconds` query parameter controls how much audio accumulates before an interim result is emitted. Default `0.5`, range `0.1`–`5.0`.
-   **Fixed** Utterances shorter than roughly 1.7 seconds could complete with no transcript and no error. The audio buffer was measured before each incoming chunk was appended, and the first chunk was never evaluated, so short answers never reached the recognizer.
-   Rapid audio frames arriving during connection setup could each start their own initialization, discarding the WAV metadata from the first frame. Every subsequent frame then failed with `Not a valid WAV file (missing RIFF header)` for the life of the connection. Clients sending 10–20 ms frames were affected on every session.
-   **Deprecated** `WS /websocket/speech-to-text` remains deprecated and receives no new recognition features. New integrations should use `WS /api/v1/listen`.

July 24, 2026 v1

## New capabilities

-   **New** New Text-to-Speech capabilities.
-   New Speech-to-Text capabilities.


---

# Status

> 90 days of measured uptime for every region and service.

## 90-day uptime

One bar per UTC day, oldest first. Continuous probing began 28 July 2026; earlier days reflect our own records.

The bars render on the page itself. For the current state as JSON — every service and region, probed live — fetch `https://docs.munsit.com/api/status`.

