Realtime Text Stream Translation API
Low-latency translation of streamed text over a single WebSocket connection — an incremental engine built specifically for text that arrives piece by piece. Push text fragments as they are produced (word by word, phrase by phrase); the engine accumulates them in a translation context and emits translations as soon as it decides a piece is final.
Use it to translate live captions, a user typing, chat messages, or the output of your own ASR — anything where text arrives gradually and you want the translation to start before the sentence ends.
Palabra API client
Consider using the Palabra API Python client
(Palabra.text_translation()).
from palabra_ai import Palabra, TextTranslation
palabra = Palabra() # $PALABRA_API_KEY
async with palabra.text_translation("en", "es") as session:
await session.push_text("The weather ")
await session.push_text("is beautiful ")
await session.flush("today.") # finalize the utterance
async for event in session:
if isinstance(event, TextTranslation):
print(event.text) # "El clima es hermoso hoy."
break
Step 1. Get an API Key
Create an API Key on the Palabra API Keys page. See Authentication for details.
Step 2. Connect
Open a WebSocket to the endpoint below, passing your API Key as the token
query parameter (or in the Authorization header). The server validates the
key and creates a streaming session for the lifetime of the connection
automatically. Session JWTs and ClientId/ClientSecret pairs are accepted the
same way as in the other realtime APIs.
wss://stream.palabra.ai/text-api/v1/text-translation/stream?token=<API_KEY>
Step 3. Configure the session
All messages in both directions are JSON text frames with one envelope:
{ "type": "<message type>", "message_id": "<opaque string>", "data": { } }
message_id is chosen by you (e.g. an incrementing counter as a string) and is
echoed back on the response that corresponds to the message — see
Correlation.
Start by sending set_settings:
{
"type": "set_settings",
"message_id": "1",
"data": {
"lang_src": "en",
"lang_tgt": "es",
"model": "smt",
"context_size": 60,
"max_sentences_in_context": 4,
"min_tokens": 5
}
}
| Field | Description |
|---|---|
lang_src / lang_tgt | Source and target language codes |
model | Translation model; smt is the incremental low-latency engine (recommended and required for flush) |
context_size | How many tokens of context the engine keeps (0–60) |
max_sentences_in_context | Sentence cap of the context (1–30) |
min_tokens | Minimal fragment size the engine translates eagerly (3–10) |
style | Optional style hint for the translation |
request_id | Optional context name — pass the same value after a reconnect to resume the accumulated context (it is scoped to your account; other clients can never read it) |
The server confirms with settings_response:
{
"type": "settings_response",
"message_id": "1",
"data": { "request_id": "…", "code": "stream_created", "message": "New translation session created", "model": "smt" }
}
set_settings can be sent again mid-session to adjust parameters (the language
pair of an existing context cannot change).
Step 4. Send text
{ "type": "translate", "message_id": "7", "data": { "sentence_src": "The weather is ", "flush": false } }
sentence_src— the next text fragment, up to 5000 characters. Fragments are concatenated by the engine exactly as sent, so they do not have to be whole words — raw ASR deltas that cut a word mid-way are fine. In languages that use spaces, include the spaces yourself (they are required for the joined stream to read like the original text), and keep punctuation — it drives utterance segmentation.flush: true— optional latency control. The engine emits translations on its own as the context accumulates — you never have to flush. A flush tells the engine to answer now: everything accumulated but not yet translated is translated immediately, and the engine moves on to the next utterance. Natural moments are end-of-utterance signals (sentence end, speaker turn, your ASR's end-of-speech) — flushing there cuts the latency of the tail. A flush works in both forms:- with text —
{"sentence_src": "today.", "flush": true}pushes the last fragment and finalizes in one message; - without text —
{"sentence_src": null, "flush": true}just finalizes what has already been pushed.
- with text —
To drop the accumulated context without closing the connection, send
{"type": "clear_context", "message_id": "…", "data": {}} — the server replies
with context_cleared.
Step 5. Receive translations
{
"type": "translation_result",
"message_id": "9",
"data": { "request_id": "…", "translation": "El clima es hermoso hoy.", "model_used": "smt" }
}
Correlation
The engine translates incrementally: a translate message may produce no
reply at all — that means the fragment was accumulated into the context and is
still waiting for enough material. The next translation_result you receive
covers every fragment sent up to and including the message_id it carries.
So to render a live translation, keep your fragments in send order and mark
all of them translated whenever a result with an equal-or-higher message_id
arrives.
Errors
{ "type": "error", "message_id": "7", "data": { "code": "VALIDATION_ERROR", "message": "…" } }
| Code | Meaning |
|---|---|
VALIDATION_ERROR | Malformed envelope, unknown type, missing session or an over-long fragment |
BAD_PARAM / BAD_REQUEST | The engine rejected the settings or the request (e.g. unsupported pair, language mismatch) |
RATE_LIMIT_EXCEEDED | Too many translate messages (50 per second by default) |
SERVICE_UNAVAILABLE | The translation backend is unavailable — reconnect and re-send set_settings (use the same request_id to resume the context) |
QUOTA_EXCEEDED | Billing limit reached; the connection is closed |
Limits & billing
- One fragment ≤ 5000 characters;
translaterate limit 50/second. - Connection rate limit 20 per minute per token.
- Billing unit: characters of
sentence_srcat submit time.