PurpleCallio documentation
Live Translate (Early Access)
Live Translate lets two people on a 1:1 call speak different languages: each hears and reads the other in their own language, a short moment behind the speaker. It's in early access, so endpoints and fields may change before general availability.
How it works
You set each participant's language and a translation mode when you create the call. During the call, each browser streams speech to PurpleCallio, which runs speech recognition, translation and (in voice mode) synthesized speech on the server through Soniox. Both participants receive DualView captions; only the listener receives translated audio, and the original voice is turned down rather than muted.
Modes
off (default), captions (translated text only, lower rate) and voice (captions plus translated speech). Set the mode at call creation, change it with PATCH /calls/:id/translation, or let participants switch in the call. Voice is offered only for languages that pass our testing; others fall back to captions.
Data handling
No audio or transcript is stored by default. Replay of the original audio is off by default and opt-in per project; when on, it uses a memory-only buffer of about 30 seconds that is cleared at the end of the call. Transcripts are stored only if you enable the Post-Call Suite, for the retention period you choose.
Early access. Live Translate is enabled per account during early access. Endpoints, fields and limits below may change before general availability.
Enable translation on POST /calls
Add a translationblock and each participant's language when you create the call. Calls without it, or with mode: "off", behave exactly as before, and a call is never refused because of translation when the mode is off.
curl -X POST https://purplecallio.com/api/calls \
-H "x-api-key: $PURPLECALLIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callerId": "doc_1",
"receiverId": "pat_9",
"callerLanguage": "en",
"receiverLanguage": "mr",
"translation": {
"mode": "voice",
"lexiconId": "lex_123",
"context": {
"industry": "healthcare",
"purpose": "post-surgery follow-up",
"roles": { "caller": "doctor", "receiver": "patient" }
},
"postCallReport": true,
"maxMinutes": 30,
"guard": { "enabled": true, "sensitivity": "standard", "replay": false },
"onLimit": "captions_then_off"
}
}'| Field | Meaning |
|---|---|
| mode | off | captions | voice. Default off. captions = text only; voice = captions plus translated speech. |
| callerLanguage / receiverLanguage | Top-level fields. ISO code with optional region (hi, en-IN) or auto to detect the speaker's language. |
| lexiconId | PurpleLex lexicon to apply. Must belong to the API key's project. |
| context | { industry, purpose (≤ 200 chars), roles: { caller, receiver } }. Helps the model read ambiguous words; set once per session. |
| postCallReport | Request a Post-Call Suite report. Only effective if the project has the Post-Call Suite enabled. |
| maxMinutes | CostGuard per-call translation limit, 1–600. |
| guard | { enabled, sensitivity: "standard", replay }. replay defaults to false and only works if the project allows replay. |
| onLimit | captions_then_off (voice steps down to captions, then off) or off. |
The response's participants[] gain a language, and the top level gains translation: { mode, config }.
Change translation during a call
PATCH /calls/:id/translation(API key or the participant's session) changes the mode or languages; live sessions apply it straight away.
curl -X PATCH https://purplecallio.com/api/calls/$CALL_ID/translation \
-H "x-api-key: $PURPLECALLIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "mode": "captions", "receiverLanguage": "ta" }'
# → { "mode": "captions", "callerLanguage": "en", "receiverLanguage": "ta" }Browser SDK: TranslationClient
The browser connects to the /translateSocket.IO namespace with the participant's call session token (bj_session_…). TranslationClient from @purplecallio/sdk handles the microphone, voice activity detection, playback and ducking of the original voice. Audio is sent only while the person is speaking.
import { TranslationClient } from "@purplecallio/sdk";
// token = this participant's call session token (bj_session_…)
const translation = new TranslationClient({
serverUrl: "https://purplecallio.com/api",
token,
speak: "mr", // or "auto"
hear: "en",
mode: "voice", // "captions" | "voice"
originalVolume: 0, // other person's original voice while it's translated (0 = muted)
remoteAudio: remoteAudioElement,
});
await translation.enable();
translation.on("caption", (p) => render(p.phraseId, p.original, p.translated));
translation.on("flagged", (p) => markUncertain(p.phraseId, p.guard?.reasons));
translation.on("repeat.requested", ({ prompt }) => showPrompt(prompt));
translation.on("limit", ({ kind, message }) => showBanner(kind, message));
translation.requestRepeat(phraseId); // ask the speaker to say it again
translation.replay(phraseId); // only if the project enabled replay
translation.setMode("captions");Events: ready, caption (repeated per phraseId as text grows, with startedAt, endedAt and translatedAudioAt for DualView timing), flagged, repeat.requested, limit, mode, peer, playback, error and ended. Captions go to both participants; translated audio only to the listener.
React: Translation UI Kit
import {
DualViewCaptions,
LanguagePicker,
TranslationControls,
useTranslation,
} from "@purplecallio/react";
export function TranslatedCall({ token }: { token: string }) {
const translation = useTranslation({ token, speak: "en", hear: "en", mode: "voice" });
return (
<>
<LanguagePicker translation={translation} />
<TranslationControls translation={translation} />
<DualViewCaptions translation={translation} layout="both" />
</>
);
}| Component | What it does |
|---|---|
| <DualViewCaptions /> | Original and translated lines in sync with the audio, amber MeaningGuard flags, Ask to repeat, Replay (when enabled) and a layout switch. |
| <TranslatedChatView /> | Side-by-side chat bubbles, original on one side and translation on the other, scrollable through the call. |
| <LanguagePicker /> | Searchable languages in their own script with voice and captions badges from GET /translation/languages. |
| <TranslationControls /> | Mode switch (off, captions, voice), original-volume slider, CostGuard banner and a headphones tip. |
| useTranslation() | Hook exposing translation state, phrases and actions for your own UI. |
Non-Latin scripts use Noto Sans fonts loaded only for languages in the call; Arabic and Urdu render right-to-left.
PurpleLex: lexicons
A lexicon is a project's list of terms. Each term is DO_NOT_TRANSLATE, TRANSLATE_AS (per target language) or PRONOUNCE_AS (a hint for the spoken voice). Pick one per call with lexiconId.
# Create a PurpleLex lexicon
curl -X POST https://purplecallio.com/api/translation/lexicons \
-H "x-api-key: $PURPLECALLIO_API_KEY" -H "Content-Type: application/json" \
-d '{ "name": "Clinic terms" }'
# Add or update terms (bulk, up to 2,000)
curl -X PUT https://purplecallio.com/api/translation/lexicons/$LEXICON_ID/terms \
-H "x-api-key: $PURPLECALLIO_API_KEY" -H "Content-Type: application/json" \
-d '[
{ "term": "CloudDesk", "kind": "DO_NOT_TRANSLATE" },
{ "term": "follow-up", "kind": "TRANSLATE_AS", "translations": { "hi": "फ़ॉलो-अप", "mr": "फॉलो-अप" } },
{ "term": "Dr. Iyer", "kind": "PRONOUNCE_AS", "pronunciation": "doctor eye-yer" }
]'CSV import (POST /translation/lexicons/:id/import with { csv }) uses the columns term,kind,lang:hi,lang:ta,…,pronunciation:
term,kind,lang:hi,lang:mr,pronunciation
CloudDesk,DO_NOT_TRANSLATE,,,
follow-up,TRANSLATE_AS,फ़ॉलो-अप,फॉलो-अप,
Dr. Iyer,PRONOUNCE_AS,,,doctor eye-yerIndustry packs: GET /translation/lexicons/packs lists starter lexicons (healthcare, legal, B2B SaaS, finance, e-commerce); POST /translation/lexicons/from-pack with { industry, name? } copies one into your project. Other endpoints: GET|PATCH|DELETE /translation/lexicons/:id and DELETE /translation/lexicons/:id/terms/:termId. Up to 2,000 terms per bulk upsert.
MeaningGuard: flags and recovery
MeaningGuard flags phrases that may be misunderstood. A flagged phrase arrives as a flagged event ({ phraseId, reasons }) and on the caption as guard: { status: 'uncertain', reasons }. There is never a percentage or confidence score. UI copy should say "May not be accurate — check". A phrase that isn't flagged can still be wrong, and flags never hold back a translation.
| Reason | Meaning |
|---|---|
| term_mismatch:number / amount / date / phone | A number, amount, date or phone number differs between the original and the translation. |
| term_mismatch:lexicon | A PurpleLex term was lost or changed. |
| recognition_failed | Speech was detected but nothing usable was recognised. |
| low_asr_confidence | The speech recogniser reported low confidence (only on routes that expose a reliable signal). |
| asr_alternatives_disagree | Alternative recognitions disagree on key words. |
| length_mismatch | The translation is much shorter or longer than the original, or looks truncated. |
| language_mismatch | The detected language isn't the one expected. |
| poor_audio | Low level, clipping or dropped audio frames. |
| late_translation | The translation arrived very late. |
| provider_error | The translation provider returned an error or was retried. |
Recovery: requestRepeat(phraseId) sends the speaker a prompt in their own language with their original phrase.
Replay of the original audio is off by default.A project owner can allow it (Dashboard → Project → Translation → "Allow replay of original audio"). When allowed, the server keeps about 30 seconds of audio per speaker in memory only, never written to disk and cleared at the end of the call, and both people see a "Replay enabled" indicator. A call can turn replay off with guard.replay: falsebut can't turn it on if the project doesn't allow it.
CostGuard: budgets, limits and estimates
Translation is billed per minute for each person who has translation on (one side 1×, both sides 2×) at the voice or captions rate, in credits. Rates are set by PurpleCallio and shown on the pricing page and in your dashboard; a session keeps the rate it started with. Starting translation needs credits for at least one minute of the chosen mode.
- Per-call limit:
translation.maxMinutes, plus a project default. - Project budgets: daily and monthly credit caps and alert thresholds in
GET|PATCH /translation/settings(clamped to the maximum PurpleCallio allows). - Checked on the server at start and every few seconds while translating. At about 90% of a limit, both people get a warning.
- At the limit: with
onLimit: "captions_then_off", voice steps down to captions; otherwise translation pauses. The call itself is never ended. - Budget thresholds trigger a dashboard notification and the
translation.budget.thresholdwebhook.
Price preview (public; signed-in users see their own rate):
GET /translation/estimate?mode=voice&minutes=10&calls=100&listeners=2
{ "mode": "voice", "ratePerMinute": …, "listeners": 2, "creditsPerCall": …,
"totalCredits": …, "approxInrPerCall": …, "approxInrTotal": …, "note": "…" }Translation Observability: metrics
GET /translation/metrics?from&to&pair&route returns totals, latency (time to first translated audio and caption delay, p50 and p95), reliability (provider failures, fallbacks, dropped frames) and MeaningGuard counts, overall and per language pair. Metrics are aggregates; no transcript text is included. GET /translation/languages lists languages with separate voice and captions flags.
GET /translation/metrics?from=2026-10-01&to=2026-10-31&pair=mr-en
{ "totals": { "calls": 412, "voiceMinutes": 3820, "captionsMinutes": 640, ... },
"latency": { "firstAudioMsP50": ..., "captionDelayMsP95": ... },
"guard": { "flags": 37, "byReason": { "term_mismatch:amount": 9 }, ... },
"byPair": [ { "pair": "mr-en", "route": "soniox", ... } ] }Post-Call Suite: reports
Opt-in. When the project enables the Post-Call Suite and the call doesn't set postCallReport: false, final phrases are stored (with keys, emails and card numbers redacted) for your retention period, and a report is built after the call. A report charge in credits applies once per report.
GET /calls/:id/translation-report returns { status: 'pending' | 'ready' | 'failed', transcript, summary, signals, creditsCharged, createdAt }, and call.translation.report_readyfires when it's done. The summary has key points, decisions and action items (each linked to a phraseId) in each participant's language. Signals include talk ratio, topics, flagged moments, replays and repeats.
Tone is optional and off by default, and always off for healthcare projects. When on, it's worded as "possible tone" with supporting quotes, never as a fact about a person.
Webhooks
Sent to your project webhook URL with the usual signature, the call payload and a data object.
| Event | data |
|---|---|
| call.translation.started | { mode, callerLanguage, receiverLanguage } |
| call.translation.ended | { voiceMinutes, captionsMinutes, credits, firstAudioMsP50, flags, replays, repeats, fallbacks } |
| translation.limit_reached | { scope, action } |
| translation.budget.threshold | { period, threshold, usedCredits, budgetCredits } (project-level; callId is null) |
| call.translation.report_ready | { status } |
Error codes
| Code | Status | Meaning |
|---|---|---|
| TRANSLATION_INVALID_LANGUAGE | 400 | A language code isn't valid. |
| TRANSLATION_INVALID_CONFIG | 400 | The translation block is invalid. |
| TRANSLATION_LEXICON_NOT_FOUND | 400 | lexiconId doesn't exist in this project. |
| TRANSLATION_NOT_IN_PLAN | 403 | Translation isn't unlocked for this account yet (it unlocks with your first credit purchase). |
| TRANSLATION_INSUFFICIENT_CREDITS | 402 | Not enough paid credits for one minute of the chosen mode. Free credits don't cover translation. |
| TRANSLATION_REPORT_NOT_FOUND | 404 | No Post-Call Suite report for this call. |
| TRANSLATION_DISABLED | socket | Translation isn't enabled for this call or server. |
| TRANSLATION_SESSION_INVALID | socket | The session token isn't valid for this call. |
| TRANSLATION_CALL_ENDED | socket | The call has already ended. |
| TRANSLATION_REPLAY_DISABLED | socket | Replay was requested but the project hasn't enabled it. |
Limits
- 1:1 calls only (caller and receiver).
maxMinutes: 1–600 per call.context.purpose: up to 200 characters.- Lexicon bulk upsert: up to 2,000 terms per request.
- Audio frames: 16 kHz PCM16 mono, up to 32,000 bytes per frame and 100 frames per second; translated audio is 24 kHz.
- Concurrent translated calls per account are limited during early access.
- There is a short delay of a second or more before the listener hears the translation. We publish measured numbers at launch.
Still having trouble?
If you're having an issue with this integration, our support team can help.
Contact SupportRelated documentation
Build with PurpleCallio
Explore the documentation to choose hosted UI, React components, or the headless SDK for your integration.