API reference

Arabic speech-to-text and text-to-speech behind a single API key. One key works for both.

Base URL

https://sa-gateway.tryhamsa.com

Authentication

Pass your API key on every request, either header works:

x-api-key: YOUR_KEY
# or
Authorization: Bearer YOUR_KEY

Keys are issued and revoked from the console. Missing, invalid, or revoked keys return 401.

Billing

Usage is metered in audio-seconds — the same unit for both services. STT bills the duration of audio you send; TTS bills the duration of audio generated. Track it in the console.

GET/v1/voices

Lists the 113 built-in voices and dialect codes, plus your custom voices ("custom": true). Use a voice id as the TTS speaker.

curl https://sa-gateway.tryhamsa.com/v1/voices -H "x-api-key: $HAMSA_KEY"
# -> { "count": 113, "dialects": [...],
#      "data": [ {"id": "Layla", "dialect": "egy"}, ... ] }
dialects
codecode
msaStd ArabicirqIraqi
egyEgyptianjorJordanian
ksaSaudiplsPalestinian
uaeEmiratilebLebanese
kuwKuwaitisyrSyrian
qatQataribahBahraini
omaOmanienEnglish

POST/v1/stt

Transcribe audio. Send a base64-encoded WAV; get back text and the billed duration. Add the flags below for gender, diarization, or turn detection.

request body
fieldtype
audiostringbase64-encoded WAV required
langstringar (default), en, auto to detect from the audio, or ar-en for Arabic with English mixed in
promptstringtext the model is conditioned on before decoding: names, product terms and English words you expect (in the script you want them written in). The standard lever for domain vocabulary and code-switching.
gender_detectionboolalso return speaker gender
speaker_identificationboolalso return a speaker embedding (diarization)
eos_enabledboolalso return end-of-speech / turn detection
curl
curl -X POST https://sa-gateway.tryhamsa.com/v1/stt \
  -H "x-api-key: $HAMSA_KEY" \
  -H "content-type: application/json" \
  -d '{"audio":"<base64-wav>","lang":"ar"}'
python
import base64, requests
audio = base64.b64encode(open("clip.wav","rb").read()).decode()
r = requests.post("https://sa-gateway.tryhamsa.com/v1/stt",
    headers={"x-api-key": KEY},
    json={"audio": audio, "lang": "ar"})
print(r.json())   # {"text": "...", "duration": 3.5}
response
{"text": "مرحبا بك في همسة", "duration": 3.5}

# with gender_detection / speaker_identification / eos_enabled:
{"text": "...", "duration": 3.5, "gender": "Female",
 "eos": {"prediction": 1, "probability": 0.82}, "speaker": "<embedding>"}

POST/v1/tts

Synthesize speech. Returns a stream of audio bytes: 16 kHz WAV, or raw 8 kHz µ-law (no header) when mulaw is true.

request body
fieldtype
textstringtext to synthesize required — supports <pause_short> / <pause_long>
speakerstringvoice id, case-insensitive (see /v1/voices, or a custom voice) required
language_idstringvoice language id required
dialectstringdialect code (egy, ksa, uae, …); auto if omitted
expressivenessfloat0.0–2.0, default 1.0 (neutral → emotional)
speedfloat0.5–2.0, default 1.0
mulawboolraw 8 kHz G.711 µ-law, mono, no header (audio/basic); default false = 16 kHz WAV
curl
curl -X POST https://sa-gateway.tryhamsa.com/v1/tts \
  -H "x-api-key: $HAMSA_KEY" \
  -H "content-type: application/json" \
  -d '{"text":"مرحبا بالعالم","speaker":"layla","language_id":"ar"}' \
  --output out.wav
python (stream to file)
import requests
with requests.post("https://sa-gateway.tryhamsa.com/v1/tts",
        headers={"x-api-key": KEY},
        json={"text":"مرحبا بالعالم","speaker":"layla","language_id":"ar"},
        stream=True) as r:
    with open("out.wav","wb") as f:
        for chunk in r.iter_content(8192):
            f.write(chunk)
TTS streaming over plain HTTP (a WebSocket is also available)

The response is a chunked stream, so you do not have to wait for the whole file. Request raw audio (mulaw here, or response_format=pcm / mulaw on /v1/audio/speech), read the body as it arrives and start playback on the first chunk: first audio lands ~0.5 s after the request regardless of sentence length, and the remaining audio streams faster than real time. Any HTTP client that exposes the response body as a stream works (requests(stream=True), fetch + ReadableStream, HttpClient with HttpCompletionOption.ResponseHeadersRead).

POST/v1/voice-clone

Create a custom voice from a reference recording and use it like any built-in voice. Send a clean, single-speaker clip of 3–30 s (≤ 25 MB; wav, mp3, ogg, flac, m4a, aac, webm) together with its exact transcript. Voice-clone calls are not billed.

three ways to send the reference audio
you sendbest forendpoint
1 · base64the file, base64-encoded, inside the JSON bodyaudio you already hold in your backend; nothing to hostPOST /v1/voice-clone
2 · media URLa public or pre-signed https URL; we download itrecordings already in S3 / GCS / a CDN; smallest requestPOST /v1/voice-clone
3 · uploada multipart/form-data filebrowser forms, Postman, quick manual testsPOST /v1/voice-clone/upload
fields — JSON body for options 1–2, form fields for option 3
fieldtype
prompt_textstringexact transcript of the reference clip required
audio_base64stringoption 1 — the reference audio, base64-encoded
audio_urlstringoption 2 — public http(s) URL of the reference audio (send exactly one of audio_base64 / audio_url)
filefileoption 3 — the reference audio file (multipart)
voice_idstringname for the voice: 1–64 chars, letters, digits, _, -; stored lower-case. Generated when omitted. Built-in names are reserved; re-cloning your own id replaces it.
dialectstringdefault dialect for the voice (see /v1/voices), default msa
option 1 — base64 · python
import base64, requests

with open("reference.wav", "rb") as f:
    audio_b64 = base64.b64encode(f.read()).decode()

r = requests.post("https://sa-gateway.tryhamsa.com/v1/voice-clone",
    headers={"x-api-key": KEY},
    json={
        "voice_id":     "agent_sara",
        "dialect":      "ksa",
        "prompt_text":  "<exact transcript of the clip>",
        "audio_base64": audio_b64,
    })
print(r.json())   # {"voice_id": "agent_sara", "registered": true, ...}
option 1 — base64 · curl (build the body with jq, so the long string needs no quoting)
jq -n --arg audio "$(base64 < reference.wav | tr -d '\n')" \
      --arg text  "<exact transcript of the clip>" \
      '{voice_id:"agent_sara", dialect:"ksa", prompt_text:$text, audio_base64:$audio}' > clone.json

curl -X POST https://sa-gateway.tryhamsa.com/v1/voice-clone \
  -H "x-api-key: $HAMSA_KEY" -H "content-type: application/json" \
  --data @clone.json
option 2 — media URL
curl -X POST https://sa-gateway.tryhamsa.com/v1/voice-clone \
  -H "x-api-key: $HAMSA_KEY" -H "content-type: application/json" \
  -d '{
        "voice_id":    "agent_sara",
        "dialect":     "ksa",
        "prompt_text": "<exact transcript of the clip>",
        "audio_url":   "https://storage.example.com/recordings/sara.wav?X-Amz-Signature=..."
      }'

The URL must be reachable from the internet without a login (a pre-signed S3 / GCS URL is ideal). Same size, length and format limits as option 1. Private-network and non-http(s) addresses are refused.

option 3 — multipart upload
curl -X POST https://sa-gateway.tryhamsa.com/v1/voice-clone/upload \
  -H "x-api-key: $HAMSA_KEY" \
  -F file=@reference.wav \
  -F prompt_text="<exact transcript of the clip>" \
  -F voice_id=agent_sara -F dialect=ksa
response — identical for all three options
{
  "voice_id":   "agent_sara",
  "registered": true,
  "dialect":    "ksa",
  "replaced":   false,
  "usage":      "pass \"agent_sara\" as speaker (/v1/tts) or voice (/v1/audio/speech)"
}
then use it — exactly like a built-in voice
# OpenAI-compatible
curl -X POST https://sa-gateway.tryhamsa.com/v1/audio/speech \
  -H "Authorization: Bearer $HAMSA_KEY" -H "content-type: application/json" \
  -d '{"input":"مرحبا، كيف أقدر أساعدك؟","voice":"agent_sara"}' --output out.wav

# native
curl -X POST https://sa-gateway.tryhamsa.com/v1/tts \
  -H "x-api-key: $HAMSA_KEY" -H "content-type: application/json" \
  -d '{"text":"مرحبا، كيف أقدر أساعدك؟","speaker":"agent_sara","language_id":"ar"}' --output out.wav
manage
curl https://sa-gateway.tryhamsa.com/v1/voices -H "x-api-key: $HAMSA_KEY"                       # built-in + yours ("custom": true)
curl -X DELETE https://sa-gateway.tryhamsa.com/v1/voices/agent_sara -H "x-api-key: $HAMSA_KEY"
errors
statuswhen
400no audio, or both options at once · clip shorter than 3 s or longer than 30 s · unreadable audio · voice_id invalid or a built-in name · URL not http(s) or unreachable
409voice_id already belongs to another API key
413reference larger than 25 MB

Custom voices are usable immediately and persist: the gateway keeps them and re-registers them automatically after a model restart. Quality follows the reference: a quiet room, one speaker, natural pace, and a transcript that matches the audio word for word.

Batch transcription — async, with diarization

For full recordings (calls, meetings, media): submit a job, poll for the result. The completed job returns the full transcript, speaker diarization with word-level timestamps, and the detected language. Billed once, on completion, by audio duration.

POST/v1/transcriptions

request body
fieldtype
audio_urlstringpublic / pre-signed URL of the media file one of these two
audio_base64stringbase64-encoded audio
languagestringar (default), en, or auto to detect from the audio
return_srt_formatboolalso return subtitles (SRT)
srt_optionsobjectmax_lines_per_subtitle, max_chars_per_line, …
curl
curl -X POST https://sa-gateway.tryhamsa.com/v1/transcriptions \
  -H "x-api-key: $HAMSA_KEY" -H "content-type: application/json" \
  -d '{"audio_url":"https://.../call.mp3","language":"ar"}'
# -> 202 {"job_id":"28b3ff340f58...","status":"IN_QUEUE"}

GET/v1/transcriptions/{job_id}

Poll until status is COMPLETED (or FAILED / CANCELLED / TIMED_OUT). Cancel with DELETE on the same path.

completed response
{
  "job_id": "28b3ff340f58...", "status": "COMPLETED", "audio_seconds": 0.52,
  "result": {
    "detected_language": "ar",
    "transcription": "مرحبا",
    "diarization": [
      { "speaker": "speaker_0", "start": 0.07, "end": 0.49, "text": "مرحبا",
        "words": [ {"word": "مرحبا", "start": 0.07, "end": 0.49, "score": 0.95, "speaker": "speaker_0"} ] }
    ],
    "usage": { "audio_length": 0.0087 }
  }
}
python (submit + poll)
import time, requests
H = {"x-api-key": KEY}
job = requests.post("https://sa-gateway.tryhamsa.com/v1/transcriptions",
    headers=H, json={"audio_url": MEDIA_URL, "language": "ar"}).json()
while True:
    r = requests.get(f"https://sa-gateway.tryhamsa.com/v1/transcriptions/{job['job_id']}", headers=H).json()
    if r["status"] in ("COMPLETED", "FAILED", "CANCELLED", "TIMED_OUT"): break
    time.sleep(3)
for seg in r["result"]["diarization"]:
    print(seg["speaker"], seg["start"], seg["end"], seg["text"])

TTS streaming (WebSocket)

Open a WebSocket to wss://sa-gateway.tryhamsa.com/v1/tts/stream and authenticate with the x-api-key header or ?api_key=YOUR_KEY. Keep one socket open for the whole call and send one synthesize message per sentence; audio comes back as binary frames as it is generated.

connect  wss://sa-gateway.tryhamsa.com/v1/tts/stream?api_key=YOUR_KEY

<- {"type":"ready", ...}
-> {"type":"synthesize","payload":{"text":"مرحبا، كيف أقدر أساعدك؟","speaker":"reem","mulaw":true}}
<- {"type":"start","sample_rate":8000,"encoding":"mulaw"}
<- <binary audio frames>                                  # play as they arrive
<- {"type":"end","audio_seconds":2.1,"elapsed_ms":1050}
-> {"type":"cancel"}                                        # barge-in: stop the current turn
synthesize payload
fieldtype
textstringtext to speak required
speakerstringvoice id — built-in (see /v1/voices) or a custom voice required
mulawbooltrue = raw 8 kHz G.711 mulaw (telephony); default false = raw PCM 16-bit little-endian
sample_ratestring"16k" (default) or "8k", for PCM output
dialect, speed, expressivenesssame meaning as on /v1/tts
request_idstringechoed back in start / end

Audio frames are raw samples with no header. Errors arrive as {"type":"error","code":"...","message":"..."} (for example speaker_not_found) and the socket stays open. Billed by generated audio seconds.

Real-time streaming (WebSocket)

For live transcription, open a WebSocket to /v1/stt/stream — use ws:// (wss:// over TLS). Authenticate with ?api_key=YOUR_KEY or the x-api-key header.

connect  wss://sa-gateway.tryhamsa.com/v1/stt/stream?api_key=YOUR_KEY

-> {"type":"handshake","options":{"gender_detection":true,"eos_enabled":true}}
<- {"type":"handshake_ack","status":"authenticated"}
-> {"type":"media","payload":"<base64 audio>"}           # or raw binary frames
<- {"type":"transcription","data":{"transcription":"...","gender":"Female"},"duration_ms":500}

Handshake options: audio_type (PCM/MULAW), sample_rate, vad_threshold, min_silence_duration_ms, eos_enabled, eos_threshold, gender_detection, speaker_identification. Billed by streamed speech-seconds.

Audio on the WebSocket is raw frames, not files. Send 16-bit little-endian PCM at 16 kHz (default) or 8 kHz, or 8 kHz G.711 mulaw, as binary frames (~20–100 ms each) and declare it in the handshake: {"type":"handshake","options":{"audio_type":"MULAW","sample_rate":8000,"language":"ar"}}. No WAV/MP3 container. Send {"type":"finalize"} to flush the last utterance. A prompt option is accepted in the handshake. The WebSocket returns text per utterance only: for speaker diarization and word timestamps use verbose_json over HTTP.

OpenAI-compatible APIs

Already on the OpenAI SDK? Point its base_url at https://sa-gateway.tryhamsa.com/v1, pass your Hamsa key, and it just works. Usage is metered the same per-key audio-seconds way.

field mapping
OpenAI→ Hamsa model
file (multipart)base64 → /transcribe audio
languagelang — ar (default), en, auto, ar-en
promptprompt (conditioning text: names, domain terms, expected English words)
input/tts/stream text
voicespeaker (your Hamsa speaker id)
speedspeed (clamped 0.5–2.0)
dialect, expressiveness (extra)passed through to the model

response_format — speech: wav (default, 16 kHz WAV) · pcm (raw 16 kHz 16-bit little-endian mono, no header) · mulaw (raw 8 kHz G.711 µ-law mono, no header, audio/basic — telephony-ready) (others → 400). Transcription: json (default) · text · verbose_json · srt · vtt. Use model="hamsa"; TTS input ≤ 2000 chars, upload ≤ 25 MB.

low-latency transcription over plain HTTP — no WebSocket needed

For conversational use, a plain POST /v1/audio/transcriptions (or /v1/stt) per utterance is already the fast path: short clips (json / text) run on the real-time engine, ~110 ms of model time for a 2 s utterance. Do the end-pointing on your side and send each utterance as soon as it closes. The WebSocket only adds partial results and server-side end-of-speech detection while the caller is still speaking; the SSE mode below is for long recordings, not for lower latency.

µ-law (telephony) audio in — POST /v1/audio/transcriptions

Send headerless 8 kHz G.711 µ-law as the file and add input_format=mulaw (a .ulaw / .mulaw / .pcmu filename or an audio/basic upload is detected automatically). µ-law inside a WAV container needs nothing extra. Note: response_format on this endpoint is the transcript format (json, text, verbose_json, srt, vtt); response_format=mulaw belongs to /v1/audio/speech. Works with every response format, with stream=true, and at any length up to the 25 MB limit.

curl -X POST https://sa-gateway.tryhamsa.com/v1/audio/transcriptions \
  -H "Authorization: Bearer $HAMSA_KEY" \
  -F file=@call.ulaw -F input_format=mulaw \
  -F language=ar -F response_format=text
streaming on /v1/audio/transcriptions

Pass stream=true to receive the response as server-sent events instead of one body. Useful for long recordings: the transcript starts arriving well before the request would otherwise return, so your HTTP client does not sit idle and time out. Works with json and verbose_json (srt/vtt → 400, since a partial subtitle file is not meaningful).

eventwhen
transcript.text.deltainterim text, emitted as soon as transcription finishes — show it live, but treat it as a preview
transcript.text.segmentone per diarized segment, with speaker, start, end
transcript.text.donefinal text; for verbose_json also segments, language, duration

Deltas are interim; transcript.text.done is authoritative. Deltas come straight off the transcription pass, before word alignment runs. The final text is the aligned one, so it can differ slightly — in practice by a few standalone punctuation tokens that alignment drops. Render deltas live for responsiveness, then replace with done.text. This is the usual interim-then-final pattern for streaming speech recognition.

Speaker labels arrive at the end, not with the deltas. Diarization clusters voices across the whole recording, so a speaker's identity is not settled until the last audio is processed. Text streams early; transcript.text.segment events follow once diarization completes. Lines beginning : are keep-alive comments — ignore them (the OpenAI SDK already does).

python (OpenAI SDK, streaming)
stream = client.audio.transcriptions.create(
    model="hamsa", file=open("call.mp3", "rb"),
    response_format="verbose_json", stream=True)

preview = ""
for event in stream:
    if event.type == "transcript.text.delta":
        preview += event.delta          # interim — display as it arrives
    elif event.type == "transcript.text.segment":
        print(event.speaker, event.start, event.text)
    elif event.type == "transcript.text.done":
        final = event.text              # authoritative — use this one
diarization on /v1/audio/transcriptions

Speaker count: detected automatically; when you know it, pass num_speakers (exact) or min_speakers / max_speakers — e.g. max_speakers=2 for a phone call — which prevents over-splitting on noisy or telephony audio. Word timestamps: add timestamp_granularities[]=word with response_format=verbose_json to get words: [{word, start, end, speaker}]. prompt (alias initial_prompt) biases vocabulary on json / text responses.

Request response_format=verbose_json and every segment carries a speaker label (SPEAKER_00, SPEAKER_01, …) alongside start/end/text — full multi-speaker diarization in one synchronous call. The number of speakers is detected automatically; no hints or configuration required.

python (OpenAI SDK)
from openai import OpenAI
client = OpenAI(api_key="YOUR_HAMSA_KEY", base_url="https://sa-gateway.tryhamsa.com/v1")

# transcription
t = client.audio.transcriptions.create(
    model="whisper-1", language="ar", file=open("clip.wav", "rb"))
print(t.text)

# speech (voice = your Hamsa speaker id)
with client.audio.speech.with_streaming_response.create(
        model="tts-1", voice="layla", input="مرحبا بالعالم") as r:
    r.stream_to_file("out.wav")
curl — POST /v1/audio/transcriptions (diarized)
curl https://sa-gateway.tryhamsa.com/v1/audio/transcriptions \
  -H "Authorization: Bearer $HAMSA_KEY" \
  -F file=@call.wav -F model=hamsa -F language=ar \
  -F response_format=verbose_json
# -> { "text": "...", "language": "ar",
#      "segments": [
#        { "speaker": "SPEAKER_01", "start": 0.7,  "end": 12.0, "text": "..." },
#        { "speaker": "SPEAKER_00", "start": 15.8, "end": 17.6, "text": "..." }
#      ] }
curl — POST /v1/audio/speech (streams — play from the first chunk with response_format=pcm or mulaw, see low latency over HTTP)
curl https://sa-gateway.tryhamsa.com/v1/audio/speech \
  -H "Authorization: Bearer $HAMSA_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"tts-1","voice":"layla","input":"مرحبا بالعالم"}' \
  --output out.wav

Throughput and limits

Transcription runs at roughly 17x realtime, and that rate holds steady from short clips to the largest accepted upload. To estimate any file: processing seconds ≈ audio seconds / 17.

audiouploadfull responsefirst text with stream=true
1 min0.6 MB~6 s~2 s
36 min17 MB~127 s~27 s
53 min24 MB~188 s~40 s

The 25 MB cap works out to roughly 50 minutes of typical compressed speech. One transcription runs at a time with a second admitted and queued behind it; a third concurrent request receives 429 with Retry-After rather than waiting indefinitely. Retry on 429, or use the asynchronous job API below, which has no such limit.

For recordings beyond a couple of minutes, prefer stream=true: the total time is the same, but text begins arriving after about a fifth of the wait and a keep-alive is sent every 5 seconds, so long requests do not sit idle against a client timeout.

choosing between synchronous and asynchronous
synchronous /v1/audio/transcriptionsasynchronous /v1/transcriptions
you getthe result on the same connectiona job_id to poll
startsimmediately, on a warm GPUafter ~1–2 min while a GPU is provisioned for your job
concurrency1 running, 1 queued, then 429unlimited, each job gets its own GPU
best foranything interactive, and files under ~15 minlong recordings, bulk backlogs, many files at once

Asynchronous jobs run on a GPU created for that job and released afterwards, which is why they carry a fixed start-up cost but no concurrency limit. Submit a hundred at once if you need to. Results are stored and returned by GET /v1/transcriptions/{job_id}, in the same shape as verbose_json.

Errors

statusmeaning
400unsupported response_format, or stream=true combined with srt/vtt
401missing, invalid, or revoked API key
413upload exceeds the 25 MB limit — compress, split into ≤25 MB chunks (avoid splitting mid-sentence), or use the async POST /v1/transcriptions job API for long recordings
422invalid request body (missing required field)
429transcription capacity reached — retry after the Retry-After interval
503transcription temporarily unavailable — retryable, see Retry-After
4xx / 5xxerror from the model — body is passed through

Prefer to click around? The interactive docs let you try every endpoint in the browser.