RELAY Client

View as MarkdownOpen in Claude

What Is RELAY?

RELAY is SignalWire’s real-time WebSocket protocol for programmatic call control. While the agent-based approach (AgentBase + SWML) lets SignalWire’s AI handle conversations declaratively, RELAY gives you imperative, event-driven control over every aspect of a call.

When to Use RELAY vs Agents

ApproachBest For
AgentBase (SWML)AI-driven conversations, voice bots, structured workflows
RELAY ClientIVR systems, call routing, call center logic, recording pipelines, custom media flows

Use RELAY when you need fine-grained control over call flow — answering, playing prompts, collecting digits, recording, bridging, conferencing — without an AI agent in the loop.

Installation

The RELAY client is included in the signalwire package:

$pip install signalwire

It requires the websockets library (installed automatically as a dependency).

Quick Start

relay_hello.py

1#!/usr/bin/env python3
2"""Minimal RELAY example: answer and play a greeting."""
3
4from signalwire.relay import RelayClient
5
6client = RelayClient(
7 project="your-project-id",
8 token="your-api-token",
9 host="your-space.signalwire.com",
10 contexts=["default"],
11)
12
13@client.on_call
14async def handle_call(call):
15 await call.answer()
16 action = await call.play([{"type": "tts", "text": "Hello from RELAY!"}])
17 await action.wait()
18 await call.hangup()
19
20client.run()

Authentication

The RELAY client supports two authentication methods:

Project + API Token

LanguageSyntax
PythonRelayClient(project="...", token="...", host="...")
TypeScriptnew RelayClient({ projectId: '...', apiToken: '...', host: '...' })
1client = RelayClient(
2 project="your-project-id",
3 token="your-api-token",
4 host="your-space.signalwire.com",
5)

JWT Token

1client = RelayClient(
2 jwt_token="your-jwt-token",
3 host="your-space.signalwire.com",
4)

Environment Variables

All credentials can be provided via environment variables instead of constructor arguments:

VariablePurpose
SIGNALWIRE_PROJECT_IDProject ID
SIGNALWIRE_API_TOKENAPI token
SIGNALWIRE_JWT_TOKENJWT token (alternative to project+token)
SIGNALWIRE_SPACESpace hostname (e.g., your-space.signalwire.com)
1# With env vars set, no arguments needed
2client = RelayClient(contexts=["default"])

Connection Lifecycle

The RELAY client manages a persistent WebSocket connection with automatic reconnection:

  1. Connect — Establishes WebSocket to wss://<host>
  2. Authenticate — Sends signalwire.connect with credentials
  3. Subscribe — Registers for events on specified contexts
  4. Event Loop — Processes events until disconnected
  5. Reconnect — Automatic reconnection with exponential backoff (1s to 30s max)

Contexts

Contexts determine which inbound calls your client receives. Pass them in the constructor or subscribe dynamically:

1# Subscribe at connection time
2client = RelayClient(contexts=["sales", "support"])
3
4# Or subscribe dynamically after connecting
5await client.receive(["new-context"])
6
7# Unsubscribe from contexts
8await client.unreceive(["old-context"])

Async Context Manager

1async with RelayClient(contexts=["default"]) as client:
2 # Connected and authenticated
3 call = await client.dial(...)
4 # Automatically disconnects on exit

Handling Inbound Calls

Register a handler with the @client.on_call decorator:

relay_inbound.py

1#!/usr/bin/env python3
2"""Handle inbound calls with RELAY."""
3
4from signalwire.relay import RelayClient
5
6client = RelayClient(contexts=["ivr"])
7
8@client.on_call
9async def handle_call(call):
10 print(f"Inbound call from {call.device}")
11 await call.answer()
12
13 # Play a menu
14 action = await call.play([
15 {"type": "tts", "text": "Press 1 for sales, 2 for support."}
16 ])
17 await action.wait()
18
19 # Collect a digit
20 collect = await call.collect(
21 digits={"max": 1, "digit_timeout": 5.0},
22 initial_timeout=10.0,
23 )
24 result = await collect.wait()
25
26 digit = result.params.get("result", {}).get("digits", "")
27 if digit == "1":
28 await call.connect([[{"type": "phone", "params": {"to_number": "+15551001000"}}]])
29 elif digit == "2":
30 await call.connect([[{"type": "phone", "params": {"to_number": "+15552002000"}}]])
31 else:
32 play = await call.play([{"type": "tts", "text": "Invalid selection. Goodbye."}])
33 await play.wait()
34 await call.hangup()
35
36client.run()

The Call object provides:

PropertyDescription
call_idUnique call identifier
node_idRELAY node handling the call
contextContext the call arrived on
direction"inbound" or "outbound"
deviceDevice info dict (type, params)
stateCurrent call state
tagClient-provided correlation tag

Making Outbound Calls

Use client.dial() to initiate outbound calls:

relay_outbound.py

1#!/usr/bin/env python3
2"""Make an outbound call with RELAY."""
3
4import asyncio
5from signalwire.relay import RelayClient
6
7client = RelayClient(contexts=["default"])
8
9async def main():
10 async with client:
11 call = await client.dial(
12 devices=[[{
13 "type": "phone",
14 "params": {
15 "from_number": "+15551234567",
16 "to_number": "+15559876543",
17 "timeout": 30,
18 },
19 }]],
20 )
21 print(f"Call answered: {call.call_id}")
22
23 action = await call.play([
24 {"type": "tts", "text": "This is an automated message from SignalWire."}
25 ])
26 await action.wait()
27 await call.hangup()
28
29asyncio.run(main())

The devices parameter supports serial and parallel dialing:

1# Serial dial: try first, then second
2devices = [
3 [{"type": "phone", "params": {"to_number": "+15551111111", "from_number": "+15550000000"}}],
4 [{"type": "phone", "params": {"to_number": "+15552222222", "from_number": "+15550000000"}}],
5]
6
7# Parallel dial: ring both simultaneously
8devices = [
9 [
10 {"type": "phone", "params": {"to_number": "+15551111111", "from_number": "+15550000000"}},
11 {"type": "phone", "params": {"to_number": "+15552222222", "from_number": "+15550000000"}},
12 ],
13]

Call Control Methods

Key call control methods across languages:

MethodPython
Answerawait call.answer()
Play TTSawait call.play([{"type": "tts", "text": "Hello"}])
Recordawait call.record(audio={...})
Hang upawait call.hangup()
Connectawait call.connect(devices)

Audio Playback

1# Play TTS
2action = await call.play([{"type": "tts", "text": "Hello!"}])
3await action.wait()
4
5# Play audio file
6action = await call.play([{"type": "audio", "url": "https://example.com/audio.mp3"}])
7
8# Control playback
9await action.pause()
10await action.resume()
11await action.volume(5.0) # -40.0 to 40.0 dB
12await action.stop()

Recording

1# Start recording
2action = await call.record(audio={"direction": "both", "format": "mp3"})
3
4# Pause/resume recording
5await action.pause()
6await action.resume()
7
8# Stop and get result
9await action.stop()
10result = await action.wait()
11url = result.params.get("record", {}).get("url", "")
12print(f"Recording URL: {url}")

Input Collection

1# Collect digits with prompt
2collect = await call.play_and_collect(
3 media=[{"type": "tts", "text": "Enter your account number."}],
4 collect={
5 "digits": {
6 "max": 10,
7 "digit_timeout": 3.0,
8 "terminators": "#",
9 },
10 },
11)
12result = await collect.wait()
13digits = result.params.get("result", {}).get("digits", "")
14
15# Standalone collect (no prompt)
16collect = await call.collect(
17 digits={"max": 4, "digit_timeout": 5.0},
18 speech={"end_silence_timeout": 2.0},
19)
20result = await collect.wait()

Detection (Answering Machine, Fax, DTMF)

1# Answering machine detection
2detect = await call.detect(
3 detect={"type": "machine", "params": {"initial_timeout": 4.5}},
4 timeout=30.0,
5)
6result = await detect.wait()
7machine_result = result.params.get("detect", {})

Bridging / Connecting

1# Bridge to another number
2await call.connect(
3 devices=[[{
4 "type": "phone",
5 "params": {"to_number": "+15559876543", "from_number": "+15551234567"},
6 }]],
7 ringback=[{"type": "ringtone", "name": "us"}],
8)
9
10# Disconnect (unbridge)
11await call.disconnect()

Conference

1# Join a conference
2await call.join_conference("team-standup", muted=False, beep="true")
3
4# Leave a conference
5await call.leave_conference("conference-id-here")

Hold / Unhold

1await call.hold()
2# ... do something ...
3await call.unhold()

Noise Reduction

1await call.denoise()
2# ... later ...
3await call.denoise_stop()

AI Agent on a Call

You can start an AI agent session on a RELAY-controlled call:

1ai = await call.ai(
2 prompt={"text": "You are a helpful assistant."},
3 languages=[{"name": "English", "code": "en-US", "voice": "rime.spore"}],
4)
5await ai.wait() # Blocks until AI session ends

The Action Pattern

Most call control methods return an Action object — an async handle for the ongoing operation:

1action = await call.play([{"type": "tts", "text": "Hello"}])
2
3# Fire-and-forget: don't wait for completion
4print(f"Play started, control_id={action.control_id}")
5
6# Or wait for completion
7result = await action.wait(timeout=30.0)
8print(f"Play finished: {result.params.get('state')}")

Common Action Methods

MethodAvailable OnDescription
wait(timeout)All actionsWait for completion
stop()Play, Record, Detect, Collect, Fax, Tap, Stream, Transcribe, AIStop the operation
pause()PlayAction, RecordActionPause playback/recording
resume()PlayAction, RecordActionResume playback/recording
volume(db)PlayAction, CollectActionAdjust volume

on_completed Callback

Every action-based method accepts an on_completed callback:

1async def on_play_done(event):
2 print(f"Play finished: {event.params.get('state')}")
3
4action = await call.play(
5 [{"type": "tts", "text": "Processing..."}],
6 on_completed=on_play_done,
7)
8# No need to await — callback fires automatically

SMS/MMS Messaging

The RELAY client supports sending and receiving SMS/MMS messages.

LanguageSend Message
Pythonawait client.send_message(to_number="+155...", from_number="+155...", body="Hello")
TypeScriptawait client.sendMessage({ toNumber: '+155...', fromNumber: '+155...', body: 'Hello' })

Sending Messages

1message = await client.send_message(
2 to_number="+15559876543",
3 from_number="+15551234567",
4 body="Hello from SignalWire RELAY!",
5)
6print(f"Message ID: {message.message_id}")
7
8# Wait for delivery confirmation
9result = await message.wait(timeout=30.0)
10print(f"Final state: {message.state}") # delivered, failed, etc.

Sending MMS (with media)

1message = await client.send_message(
2 to_number="+15559876543",
3 from_number="+15551234567",
4 body="Check out this image!",
5 media=["https://example.com/photo.jpg"],
6)

Receiving Messages

1@client.on_message
2async def handle_message(message):
3 print(f"From: {message.from_number}")
4 print(f"Body: {message.body}")
5 if message.media:
6 print(f"Media: {message.media}")

The Message object provides:

PropertyDescription
message_idUnique message identifier
from_numberSender number
to_numberRecipient number
bodyText content
mediaList of media URLs
stateCurrent state (queued, sent, delivered, failed)
direction"inbound" or "outbound"
segmentsNumber of SMS segments
tagsOptional tags

Event Listeners

Register per-call event listeners for fine-grained control:

1from signalwire.relay import EVENT_CALL_STATE, EVENT_CALL_PLAY
2
3@client.on_call
4async def handle_call(call):
5 def on_state_change(event):
6 print(f"Call state: {event.params.get('call_state')}")
7
8 def on_play_event(event):
9 print(f"Play state: {event.params.get('state')}")
10
11 call.on(EVENT_CALL_STATE, on_state_change)
12 call.on(EVENT_CALL_PLAY, on_play_event)
13
14 await call.answer()
15 action = await call.play([{"type": "tts", "text": "Hello"}])
16 await call.wait_for_ended()

Advanced Configuration

Max Active Calls

Limit concurrent calls to prevent resource exhaustion:

1# Via constructor
2client = RelayClient(contexts=["default"], max_active_calls=100)

Or via environment variable:

$export RELAY_MAX_ACTIVE_CALLS=100

Default: 1000.

Connection Limits

By default, only one RelayClient connection is allowed per process:

$export RELAY_MAX_CONNECTIONS=3

Error Handling

The client handles errors gracefully — server errors from call methods return empty dicts rather than raising exceptions. Connection-level errors trigger automatic reconnection.

For explicit error handling:

1from signalwire.relay import RelayClient, RelayError
2
3try:
4 call = await client.dial(devices=[[{...}]])
5except RelayError as e:
6 print(f"RELAY error {e.code}: {e.message}")

Complete Example: IVR System

relay_ivr.py

1#!/usr/bin/env python3
2"""Complete IVR system with RELAY."""
3
4from signalwire.relay import RelayClient, EVENT_CALL_STATE
5
6client = RelayClient(contexts=["main-ivr"])
7
8@client.on_call
9async def handle_call(call):
10 await call.answer()
11
12 # Start recording
13 recording = await call.record(audio={"direction": "both", "format": "mp3"})
14
15 # Play welcome and collect input
16 collect = await call.play_and_collect(
17 media=[
18 {"type": "tts", "text": "Welcome to Acme Corp."},
19 {"type": "tts", "text": "Press 1 for sales, 2 for support, or 3 to leave a message."},
20 ],
21 collect={"digits": {"max": 1, "digit_timeout": 5.0}},
22 )
23 result = await collect.wait()
24 digit = result.params.get("result", {}).get("digits", "")
25
26 if digit == "1":
27 await call.play([{"type": "tts", "text": "Connecting you to sales."}])
28 await call.connect([[{
29 "type": "phone",
30 "params": {"to_number": "+15551001000", "from_number": "+15550000000"},
31 }]])
32 elif digit == "2":
33 await call.play([{"type": "tts", "text": "Connecting you to support."}])
34 await call.connect([[{
35 "type": "phone",
36 "params": {"to_number": "+15552002000", "from_number": "+15550000000"},
37 }]])
38 elif digit == "3":
39 play = await call.play([
40 {"type": "tts", "text": "Please leave your message after the beep."},
41 {"type": "audio", "url": "https://example.com/beep.wav"},
42 ])
43 await play.wait()
44 voicemail = await call.record(audio={
45 "direction": "listen",
46 "format": "mp3",
47 "end_silence_timeout": 3.0,
48 })
49 await voicemail.wait()
50 else:
51 play = await call.play([{"type": "tts", "text": "Invalid option. Goodbye."}])
52 await play.wait()
53
54 # Stop recording and hang up
55 await recording.stop()
56 await call.hangup()
57
58if __name__ == "__main__":
59 client.run()

See Also

TopicReference
Agent-based approachYour First Agent