bloonio_voice_relay_client
Client SDK for bloonio_voice_relay — the Bloonio voice-assistant PaaS. Backend
integration for tenants: mint per-call tokens for your app's WebRTC/WS client, manage
voice assistants (openai / gemini / sandbox), and fetch sessions and transcripts.
Framework-agnostic (httpx + pydantic), bearer-key auth only.
Install
pip install bloonio-voice-relay-client
Two-minute integration
# .env
BLOONIO_VOICE_BASE_URL=$BASE_URL
BLOONIO_VOICE_API_KEY=bvr_...
Unlike the sibling wa / mail / auth / chat relay clients (each keyed on a
tenant_id + tenant_secret pair), the voice tenant plane accepts exactly one
credential: Authorization: Bearer bvr_.... No HMAC signing, no secondary auth mode —
BLOONIO_VOICE_API_KEY is the only secret this SDK needs.
This SDK is the backend side — read this before writing any integration code.
VoiceRelayClient/AsyncVoiceRelayClienthold your tenant'sbvr_key and must never run in a browser or ship inside a mobile app. The pattern your app's WebRTC/WS client actually needs: your server callsmint_call_token(assistant_id=...)and hands the resultingcall_tokento your app; the app then talks to the relay's public plane directly (/api/v1/public/connect/session, the call-token-gated stream socket,/api/v1/public/fetch/transcript) using only that token. Thebvr_key itself never leaves your backend.The token is session-scoped — it authorises exactly one session (connect it, stream it, read its transcript) and nothing else — and reusable within its TTL (
CALL_TOKEN_TTL_SECONDS, 300s by default server-side): it is not single-use. The TTL bounds how long the token can be used to start something (connect the call, open the stream socket); reading the transcript of a call that's already connected is honoured past that TTL for as long as the session is still live.
Quickstart
# main.py — construct the client once at startup and register it as the singleton
from fastapi import FastAPI
from bloonio_voice_relay_client import VoiceRelayClient, VoiceRelaySettings, set_voice_client
app = FastAPI()
set_voice_client(VoiceRelayClient(VoiceRelaySettings())) # reads the BLOONIO_VOICE_* env vars above
# anywhere else — a route, a worker task, a management command
from bloonio_voice_relay_client import get_voice_client
voice = get_voice_client()
voice.whoami() # confirms the key resolves: {"tenant_id": ..., "tenant_name": ...}
# provider="sandbox" is the one provider that costs nothing and calls nothing to try —
# no OpenAI/Gemini credentials needed, zero outbound network calls server-side. `openai`
# and `gemini` are the real providers; swap `provider=` once you're wiring an actual call.
assistant = voice.create_assistant(
name="Front desk",
provider="sandbox",
instructions="You are a friendly assistant for Acme Freight.",
)
print(assistant["assistant_id"], assistant["status"])
# Mint a call token for THIS assistant and hand `call_token` to your app — see
# "This SDK is the backend side" above. Never hand the app `voice` itself.
token = voice.mint_call_token(assistant_id=assistant["assistant_id"])
print(token["call_token"], token["expires_at"])
session = voice.fetch_session(session_id=token["session_id"])
print(session["status"]) # "created" -> "active" -> "ended" | "orphaned"
for turn in voice.fetch_transcript(session_id=token["session_id"]):
print(turn["role"], turn["text"])
Errors. Every method raises VoiceRelayError on any non-2xx response: .status_code,
.message (the relay's message, or the raw response text when there's no structured
message to show), and .body (the parsed JSON envelope, or None when the response
body wasn't JSON at all). .code also exists — extracted from the envelope's data.code
when present, the same mechanism bloonio_wa_relay_client uses — but is always None
against the real relay today: no voice tenant-plane route currently populates it, so an
identifier like QUOTA_EXCEEDED rides inside .message instead
("QUOTA_EXCEEDED: monthly minutes exhausted"), not as a separate field.
from bloonio_voice_relay_client import VoiceRelayError
try:
voice.mint_call_token(assistant_id=assistant["assistant_id"])
except VoiceRelayError as e:
print(e.status_code, e.message)
Reuse one VoiceRelayClient per process — it wraps a single httpx.Client — and either
call voice.close() when done or use it as a context manager: with VoiceRelayClient(...) as voice:. The async twin, AsyncVoiceRelayClient, has full method parity (await voice.whoami(), async with AsyncVoiceRelayClient(...) as voice: / await voice.aclose()).
Assistants
for a in voice.list_assistants(): # newest first, no pagination, capped at 200
print(a["assistant_id"], a["name"], a["status"])
assistant = voice.fetch_assistant(assistant_id="asst_1")
update_assistant only edits name / instructions / voice / first_message /
language / temperature / tools — provider, pipeline, mode and model are
fixed at creation and cannot be changed afterward; the update endpoint has no fields
for them at all (create a new assistant instead of trying to migrate one in place).
Fields you don't pass are left untouched, not cleared — there's no way to null a field
through this route either:
updated = voice.update_assistant(assistant_id="asst_1", name="New name", temperature=0.5)
delete_assistant always returns None — the relay's delete response carries no
data, only a confirmation message, so there's nothing to hand back:
voice.delete_assistant(assistant_id="asst_1") # -> None
Sessions & calls
create_session and mint_call_token accept the identical assistant_id request — the
difference is the response. create_session is for when your OWN backend will finish
the call itself (see connect_session below); mint_call_token (Quickstart, above) is
the one to reach for when you're instead handing the call off to your app's own
WebRTC/WS client over the public plane.
session = voice.create_session(assistant_id="asst_1")
for s in voice.list_sessions(): # newest first, no pagination/filter, capped at 200
print(s["session_id"], s["status"])
connect_session applies to provider="openai" sessions only (400 otherwise) and,
unlike every other method here, performs a real, billable server-proxied SDP exchange
with OpenAI the moment it succeeds — there is no sandbox/dry-run form of this one
call. A gemini session instead streams over a websocket (/relay/stream/session), out
of this SDK's scope. Only call it once you're intentionally starting a live OpenAI call,
with an SDP offer your own WebRTC stack produced:
result = voice.connect_session(session_id=session["session_id"], sdp_offer="v=0\r\n...")
print(result["attached"], result["sdp_answer"])
Public surface
Every client method returns the relay's response data verbatim — a plain dict or
list, never an instance of the schemas below. The schemas exist so you can
validate/type a response yourself, e.g. Assistant.model_validate(res).
| Symbol | Kind | Notes |
|---|---|---|
VoiceRelayClient |
client (sync) | wraps httpx.Client; reuse one per process |
AsyncVoiceRelayClient |
client (async) | wraps httpx.AsyncClient; async with / await .aclose() |
VoiceRelaySettings |
settings | reads BLOONIO_VOICE_* env vars (pydantic-settings); base_url / api_key required, request_timeout_seconds defaults to 10.0 |
VoiceRelayError |
error | raised on any non-2xx; .status_code / .message / .body / .code (see "Errors" above) |
AssistantProvider |
enum | openai | gemini | sandbox |
SessionStatus |
enum | created → active → ended | orphaned |
TurnRole |
enum | user | assistant |
AssistantTool |
schema | one entry of Assistant.tools — the create-side hmac_secret is write-only and never echoed back, so this schema doesn't model it either |
Assistant |
schema | create_assistant() / fetch_assistant() / list_assistants() (one row) / update_assistant(); status is a plain str, always "active" in practice (deleted rows are never returned again) |
CallToken |
schema | mint_call_token()'s response shape |
Session |
schema | fetch_session() / list_sessions() (one row) — deliberately narrower than the raw dict; internal bookkeeping fields present after a call connects are ignored, not rejected, if you validate into this schema |
ConnectSessionResult |
schema | connect_session()'s response shape |
TranscriptTurn |
schema | one row of fetch_transcript() |
set_voice_client / get_voice_client |
singleton | app-startup wiring — see "Quickstart" |
Voice ships no contract doc — the three enums above are read directly off
bloonio_voice_relay's own source, not a published spec, and none of them are formal
enum.Enum classes server-side. A future value the relay starts returning that isn't
listed here raises pydantic.ValidationError out of .model_validate() rather than
parsing leniently — client methods return the raw dict/list regardless of this, so
it only bites if you opt into the schemas above.
License
Proprietary — Bloonio internal.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bloonio_voice_relay_client-0.1.0.tar.gz.
File metadata
- Download URL: bloonio_voice_relay_client-0.1.0.tar.gz
- Upload date:
- Size: 30.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a2191873ff253c1f7bdff8ed7cf2c5796174987a797f041fc478f87067456bc
|
|
| MD5 |
4e9372d84d42aa58ef23f6928a07684c
|
|
| BLAKE2b-256 |
eb8a9c1af89aeb9aae3f9ca1ac2c3e47f76b5dc97599ded54ae5fea2bec1942a
|
File details
Details for the file bloonio_voice_relay_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: bloonio_voice_relay_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
101bcae34c8610058679102c80a2d264c4dc649de481da6b830749f06edbd6ac
|
|
| MD5 |
19ff6f1f7e84065bd06495df04bdce76
|
|
| BLAKE2b-256 |
3bb0c691aed6eda16180979b95d5d06977a95224dbdf705a5d976369294ac471
|