PurpleCallio

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.

request.sh
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"
    }
  }'
FieldMeaning
modeoff | captions | voice. Default off. captions = text only; voice = captions plus translated speech.
callerLanguage / receiverLanguageTop-level fields. ISO code with optional region (hi, en-IN) or auto to detect the speaker's language.
lexiconIdPurpleLex 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.
postCallReportRequest a Post-Call Suite report. Only effective if the project has the Post-Call Suite enabled.
maxMinutesCostGuard per-call translation limit, 1–600.
guard{ enabled, sensitivity: "standard", replay }. replay defaults to false and only works if the project allows replay.
onLimitcaptions_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.

request.sh
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.

translation.ts
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

TranslatedCall.tsx
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" />
    </>
  );
}
ComponentWhat 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.

lexicon.sh
# 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:

terms.csv
term,kind,lang:hi,lang:mr,pronunciation
CloudDesk,DO_NOT_TRANSLATE,,,
follow-up,TRANSLATE_AS,फ़ॉलो-अप,फॉलो-अप,
Dr. Iyer,PRONOUNCE_AS,,,doctor eye-yer

Industry 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.

ReasonMeaning
term_mismatch:number / amount / date / phoneA number, amount, date or phone number differs between the original and the translation.
term_mismatch:lexiconA PurpleLex term was lost or changed.
recognition_failedSpeech was detected but nothing usable was recognised.
low_asr_confidenceThe speech recogniser reported low confidence (only on routes that expose a reliable signal).
asr_alternatives_disagreeAlternative recognitions disagree on key words.
length_mismatchThe translation is much shorter or longer than the original, or looks truncated.
language_mismatchThe detected language isn't the one expected.
poor_audioLow level, clipping or dropped audio frames.
late_translationThe translation arrived very late.
provider_errorThe 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.threshold webhook.

Price preview (public; signed-in users see their own rate):

estimate.http
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.

metrics.http
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.

Eventdata
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

CodeStatusMeaning
TRANSLATION_INVALID_LANGUAGE400A language code isn't valid.
TRANSLATION_INVALID_CONFIG400The translation block is invalid.
TRANSLATION_LEXICON_NOT_FOUND400lexiconId doesn't exist in this project.
TRANSLATION_NOT_IN_PLAN403Translation isn't unlocked for this account yet (it unlocks with your first credit purchase).
TRANSLATION_INSUFFICIENT_CREDITS402Not enough paid credits for one minute of the chosen mode. Free credits don't cover translation.
TRANSLATION_REPORT_NOT_FOUND404No Post-Call Suite report for this call.
TRANSLATION_DISABLEDsocketTranslation isn't enabled for this call or server.
TRANSLATION_SESSION_INVALIDsocketThe session token isn't valid for this call.
TRANSLATION_CALL_ENDEDsocketThe call has already ended.
TRANSLATION_REPLAY_DISABLEDsocketReplay 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 Support

Related documentation

Build with PurpleCallio

Explore the documentation to choose hosted UI, React components, or the headless SDK for your integration.