Skip to main content

Install

pip install yotel
Requires Python 3.10+.

Quickstart

import os
import yotel

client = yotel.Client(api_key=os.environ["YOTEL_KEY"])

# Create campaign
c = client.campaigns.create(
    name="Q4 outreach",
    dial_mode="predictive",
    predictive_target_abandon=0.02,
)

# Push lead
lead = client.leads.create(
    campaign_id=c.id,
    phone="9876543210",
    name="Priya",
    email="priya@example.com",
)

# Bulk push (up to 500)
result = client.leads.bulk(
    campaign_id=c.id,
    leads=[
        {"phone": "9876543211"},
        {"phone": "9876543212", "name": "Arjun"},
    ],
)
print(result.valid, "added,", result.duplicates, "skipped")

Verifying webhooks

from yotel.webhook import verify_and_parse, InvalidSignature

# In your FastAPI / Flask / Django handler:
raw = await request.body()
try:
    event = verify_and_parse(
        raw,
        headers=dict(request.headers),
        signing_secret=os.environ["YOTEL_WEBHOOK_SECRET"],
    )
except InvalidSignature:
    return Response(status_code=401)

# event.event_type is typed: 'call.ended' | 'campaign.paused' | ...
if event.event_type == "call.ended":
    call_id = event.data["call_id"]
    ...

Rate-limit handling

By default the client retries 429s up to 3 times, respecting Retry-After:
# Default — let the SDK retry
client = yotel.Client(api_key="...")

# Disable — handle 429 yourself
client = yotel.Client(api_key="...", retry_on_429=False)

Async

import yotel

async with yotel.AsyncClient(api_key="...") as client:
    c = await client.campaigns.create(name="...", dial_mode="predictive")
Both sync and async clients ship in the same package.

Error handling

from yotel import (
    AuthenticationError,
    RateLimitError,
    ValidationError,
    APIError,
)

try:
    client.leads.create(campaign_id="c-1", phone="bad")
except ValidationError as e:
    print(e.errors)  # Pydantic-shaped field errors
except AuthenticationError:
    ...
except RateLimitError as e:
    print("retry after", e.retry_after)
except APIError as e:
    print(e.status_code, e.detail)

OAuth helpers

For partner apps that act on behalf of individual Yotel users, the SDK ships PKCE-ready OAuth helpers in yotel.oauth:
from yotel.oauth import AuthorizationCodeFlow, generate_code_verifier

flow = AuthorizationCodeFlow(
    client_id="sfdc_yotel",
    client_secret=os.environ["YOTEL_CLIENT_SECRET"],
    redirect_uri="https://app.example/cb",
    scopes=["campaigns:read", "openid", "offline_access"],
)
verifier = generate_code_verifier()
url = flow.build_authorize_url(state="csrf-1", code_verifier=verifier)
# ... redirect user, callback handler ...
tokens = flow.exchange_code(code=code, code_verifier=verifier)
tokens = tokens.refresh()  # rotated refresh_token
For the server-to-server JWT Bearer grant, install the oauth extra:
pip install "yotel[oauth]"
See OAuth 2.0 for the full flow reference.

Recordings

Access call recordings — signed URLs, raw bytes, or streamed to disk.
# Get a signed URL (7-day expiry)
recording = client.recordings.get_signed_url("call_id")
print(recording["url"], recording["stereo"])

# Download to disk
path = client.recordings.download("call_id", "recording.wav")

# Load into memory
audio = client.recordings.fetch_bytes("call_id")
get_signed_url requires calls:read scope. fetch_bytes and download require the separate recordings:download scope.
See Recordings API for response details.

Voice agents

Full CRUD for voice agent routing aliases.
# Create
va = client.voice_agents.create(
    name="Support bot",
    ws_url="wss://ai.example.com/ws",
    max_concurrent=50,
)

# List / Get
all_agents = client.voice_agents.list()
one = client.voice_agents.get(va.id)

# Update (PATCH)
client.voice_agents.update(va.id, max_concurrent=100)

# Archive (soft delete) or hard delete
client.voice_agents.archive(va.id)
client.voice_agents.delete(va.id)

# Tenant default
client.voice_agents.set_as_default(va.id)
default_id = client.voice_agents.get_default()
See Voice agents CRUD and Voice agent defaults.

AI sessions

Control active voice AI sessions with typed verb methods.
# Transfer call to a human agent
client.ai_sessions.transfer(
    call_id="call_abc",
    destination_type="agent",
    destination="agent_xyz",
    callback_token="yt_cb_...",
)

# Hang up
client.ai_sessions.hangup("call_abc", callback_token="yt_cb_...")

# Set disposition
client.ai_sessions.set_disposition(
    "call_abc",
    disposition="interested",
    notes="Wants premium plan",
    callback_token="yt_cb_...",
)

# Get current call state
state = client.ai_sessions.get_call_state("call_abc", callback_token="yt_cb_...")
All 25 control verbs are available as typed methods: transfer, hangup, log, mute, unmute, hold, unhold, send_dtmf, play_audio, set_disposition, recording_pause, recording_resume, get_call_state, conference_start, conference_add, conference_remove, conference_leave, request_supervisor, whisper, barge, monitor_start, monitor_stop, set_lead_field, set_lead_status, schedule_callback. See AI sessions and the Control API reference for verb parameters.

Additional error types

Beyond the base errors, voice agent and AI session operations can raise:
from yotel import (
    NoVoiceAgentConfigured,   # 424 — no agent resolved in hierarchy
    VoiceAgentInUse,          # 409 — active sessions prevent deletion
    MaxConcurrentExceeded,    # 429 — concurrency limit reached
    ConferenceMembershipRequired,  # 403 — whisper/barge needs conference
    AISessionTerminated,      # 410 — call already ended
)
See Error reference for details.

Source

github.com/kamal-zetta/dialer/tree/main/sdks/python