Skip to navigation

Queue calls

View as MarkdownOpen in Claude

A call queue holds callers until someone is free to talk to them. Callers hear hold music while they wait, and each time an agent becomes available, SignalWire connects the agent to the caller who has waited longest. This guide builds a dispatch line for Bayview Taxi: callers wait in a dispatch queue, and the dispatcher, Ada, takes them one at a time.

Choose an approach

Follow the section for your app’s approach. Each is a complete way to build the queue, and the sections don’t build on each other:

  • SWML: a document puts the caller in the queue with enter_queue, and a document on the agent’s call takes the next caller with connect. SignalWire plays the hold music and enforces the wait limit.
  • WebSocket (Relay): your code puts an answered call in the queue and connects an agent’s call to it, and handles queue events as they happen. Your code plays the hold music and decides when a caller has waited too long. No public URL is needed.

Both approaches use the same queues, which you can create, inspect, and delete over REST whichever one you choose.

Prepare for call queues

Start with working call setup from Make and receive calls. To test a queue you need two phones: one to call in and wait as the caller, and one to answer as the agent.

Trial projects only call and receive calls from verified numbers

A trial project only places calls to and receives calls from numbers you’ve purchased or verified. Prepare for your first call shows how to verify the phones you’ll test with.

Replace these values in the samples you run:

ValueReplace with
<YOUR_SPACE>Your Space’s subdomain in <YOUR_SPACE>.signalwire.com
<YOUR_PROJECT_ID>Your Project ID
<YOUR_API_TOKEN>Your API token with the Voice permission, used only in server code
<YOUR_CALLER_ID>Your SignalWire phone number, or a verified caller ID, in E.164 format
<YOUR_AGENT_NUMBER>The phone the agent answers, in E.164 format
<YOUR_BASIC_AUTH_PASSWORD>A password you choose for your SWML server
<YOUR_HOLD_MUSIC_URL>The public URL of an audio file to play while callers wait (Relay only)
<YOUR_STATUS_WEBHOOK_URL>An HTTPS endpoint you control that receives queue events
<YOUR_QUEUE_ID>A queue’s ID, from Create a queue or List queues
<YOUR_CALL_ID>The call ID of a waiting caller, from a queue event

How call queues work

A queue is a named waiting line in your project. Two calls take part in every connection:

  1. The caller’s call enters the queue. SignalWire records the caller’s position. With SWML, SignalWire also plays hold audio while the caller waits; with Relay, your code plays it. A caller can wait in only one queue at a time.
  2. The agent’s call connects to the queue. This is a separate call: an agent dialing in, or a call you place to the agent. When it connects to the queue, SignalWire takes the caller who has waited longest and bridges the two calls.
  3. The bridge ends. When either side hangs up, the other side hangs up too, unless you gave it SWML to run next with execute_after_queue.

A caller can also leave without reaching an agent: they hang up, their wait limit runs out, or your code removes them.

Create a queue

Queues are created on first use

You don’t have to create a queue first. When a call enters or connects to a queue name your project hasn’t used, SignalWire creates that queue. Names are case-sensitive, so a misspelled name creates a separate, empty queue.

Create a queue ahead of time when you want to set it up before any call arrives, or to look up its ID for the REST examples in this guide. The Server SDKs create one with a name that’s unique among your project’s queues:

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as create_queue.py and run: python create_queue.py
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
queue = client.queues.create(name="dispatch")
print(queue["id"])

To call the REST API directly, use Create queue:

POST
/api/relay/rest/queues
curl -X POST https://{your_space_name}.signalwire.com/api/relay/rest/queues \
-H "Content-Type: application/json" \
-u "<project_id>:<api_token>" \
-d '{}'

The response’s id is the queue’s ID, and friendly_name is the name that calls use to reach it. A request with a name that’s already in use fails with a validation error.

Run a call queue via SWML

enter_queue places the caller in a queue, and connect with to: "queue:<name>" connects the agent’s call to the caller at the front.

Put a caller in a queue via SWML

Callers to your number hear a greeting, then wait in the dispatch queue for up to two minutes. wait_time defaults to 180 seconds. SignalWire plays hold music until an agent connects; set wait_url to an audio file to play your own.

The server below answers calls the way Answer a call via SWML does. Run it in place of that server, with the same public URL and SWML Script.

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as dispatch_queue.py and run: python dispatch_queue.py
from signalwire import SWMLBuilder, SWMLService
service = SWMLService(
name="dispatch-queue",
route="/swml",
port=3000,
basic_auth=("signalwire", "<YOUR_BASIC_AUTH_PASSWORD>"),
# signalwire-sdk 3.4.1 validates enter_queue against an outdated schema
# that rejects every valid document, so validation is off for this service.
# https://github.com/signalwire/signalwire-python/issues/112
schema_validation=False,
)
(
SWMLBuilder(service)
.say("Thanks for calling Bayview Taxi. A dispatcher will be with you shortly.")
.enter_queue(queue_name="dispatch", wait_time=120)
# Runs only if the caller leaves the queue without reaching an agent.
.say("All our dispatchers are busy. Please call back in a few minutes.")
.hangup()
)
service.serve()

From the phone you’re using as the caller, call your SignalWire number. You hear the greeting, then hold music. Stay on the line for the next section, or wait two minutes to hear the busy message.

If you hear the busy message right after the greeting, the caller couldn’t enter the queue. When you set wait_url, check that it points at a public audio file in WAV, MP3, AIFF, GSM, or μ-law format: a file that can’t be played ends the wait.

Connect an agent to a waiting caller via SWML

Call the agent with a document that connects to the queue. When Ada answers, she hears a short announcement, then the caller who has waited longest. The call is placed as in Place a call via SWML.

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as call_agent.py and run: python call_agent.py
from signalwire import SWMLBuilder, SWMLService
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
swml = (
SWMLBuilder(SWMLService(name="call-agent"))
.say("Connecting you to the next caller in the dispatch queue.")
.connect(to="queue:dispatch")
# Runs only if no caller is waiting.
.say("No callers are waiting.")
.hangup()
.build()
)
call = client.calling.dial(
from_="<YOUR_CALLER_ID>",
to="<YOUR_AGENT_NUMBER>",
swml=swml,
)
print(call["id"])

With the caller phone still on hold, run the program and answer the agent phone. After the announcement, the hold music stops on the caller phone and the two phones can talk. Hang up either phone to end both calls.

If the agent hears “No callers are waiting.”, the queue was empty: connect doesn’t wait for a caller to arrive. It sets connect_result to failed and the document continues. Check that the caller is still on hold and that both documents use the same queue name, including case.

A queue: connect takes exactly one destination and doesn’t accept call_state_url or call_state_events; track the queue with status_url on enter_queue instead. To run more SWML on either call after the bridge ends, such as a short survey for the caller, set execute_after_queue on enter_queue or on connect to a URL or an inline SWML document.

Take a caller out of a queue via SWML

SWML has no instruction that removes a caller from a queue. A waiting caller leaves the queue in one of three ways:

How the caller leavesqueue_resultWhat happens next
An agent connects to the queueconnectedThe calls are bridged, and the caller’s document doesn’t continue
wait_time runs outtimeoutThe document continues with the next instruction
The caller hangs uphangupThe call has ended

In the caller’s document from Put a caller in a queue via SWML, the busy message plays for any caller who leaves without reaching an agent. To give a different message per reason, put a switch on queue_result after enter_queue.

To remove a caller before their wait runs out, end their call with the calling.end call command, from any process. Get the caller’s call ID from a queue event. The Server SDKs send the command with end:

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as remove_caller.py and run: python remove_caller.py
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
client.calling.end("<YOUR_CALL_ID>")

To call the REST API directly, send calling.end to Call commands:

POST
/api/calling/calls
curl -X POST https://{your_space_name}.signalwire.com/api/calling/calls \
-H "Content-Type: application/json" \
-u "<project_id>:<api_token>" \
-d '{
"command": "calling.end",
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"params": {
"reason": "hangup"
}
}'

The caller’s queue reports leave, and their call ends.

Track a queue via SWML

Add status_url to the enter_queue instruction in Put a caller in a queue via SWML to receive a JSON webhook each time the caller enters or leaves the queue. The webhooks guide covers endpoint setup and local testing.

.enter_queue(queue_name="dispatch", wait_time=120, status_url="<YOUR_STATUS_WEBHOOK_URL>")

Each webhook has event_type: calling.call.queue and reports what happened in params.status:

params.statusSent when
enqueueThe caller enters the queue
dequeueAn agent connects to the queue and takes this caller
leaveThe caller leaves without reaching an agent: wait_time runs out, the caller hangs up, or the call is ended

A caller receives either dequeue or leave, never both. SignalWire doesn’t send updates while the caller waits, so to watch positions change, read the member list.

Webhook fieldValue
params.call_idThe caller’s call ID
params.idThe queue’s ID
params.nameThe queue’s name
params.positionThe caller’s position in the queue; 0 in dequeue
params.sizeThe number of callers in the queue; 0 in dequeue
params.avg_timeThe average wait in the queue, in seconds
params.enqueue_ts, params.dequeue_ts, params.leave_tsWhen the caller entered, was taken, or left, in Unix microseconds; 0 when not applicable

The enter_queue reference has a complete payload for each event. After enter_queue, the caller’s document can also read entry_position and entry_size, the caller’s position and the queue’s size when they entered, and wait_time, how many seconds a caller who left without reaching an agent waited.

Run a call queue via WebSocket (Relay)

With Relay, your code places an answered call in a queue, then connects an agent’s call to the queue with a queue device. Your server code must play the hold music and decide when the caller has waited too long: in Relay, SignalWire plays no hold audio and doesn’t apply a wait limit.

Put a caller in a queue via WebSocket (Relay)

This handler answers each call, plays a greeting, places the caller in the dispatch queue, and loops hold music. It waits for the caller to leave the queue: a dequeue event means an agent took them, and leave means they hung up. If neither happens within two minutes, the handler removes the caller from the queue, plays a busy message, and hangs up.

The handler receives calls the way Answer a call via WebSocket (Relay) does. Run it in place of that handler, with the same topic and Relay Application.

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as dispatch_queue_relay.py and run: python dispatch_queue_relay.py
import asyncio
from signalwire.relay import RelayClient
from signalwire.relay.event import QueueEvent
GREETING = "Thanks for calling Bayview Taxi. A dispatcher will be with you shortly."
BUSY = "All our dispatchers are busy. Please call back in a few minutes."
client = RelayClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
contexts=["inbound-calling"],
)
@client.on_call
async def handle_call(call):
left_queue = asyncio.get_running_loop().create_future()
def on_queue(event: QueueEvent):
print(f"Queue: {event.status}, position {event.position}")
if event.status in ("dequeue", "leave") and not left_queue.done():
left_queue.set_result(event.status)
call.on("calling.call.queue", on_queue)
await call.answer()
greeting = await call.play([{"type": "tts", "params": {"text": GREETING}}])
await greeting.wait()
await call.queue_enter(queue_name="dispatch")
hold_music = await call.play(
[{"type": "audio", "params": {"url": "<YOUR_HOLD_MUSIC_URL>"}}], loop=0
)
try:
await asyncio.wait_for(left_queue, timeout=120)
await hold_music.stop()
except asyncio.TimeoutError:
# No agent took the caller within two minutes.
await hold_music.stop()
await call.queue_leave(queue_name="dispatch")
busy = await call.play([{"type": "tts", "params": {"text": BUSY}}])
await busy.wait()
await call.hangup()
await call.wait_for_ended()
client.run()

From the phone you’re using as the caller, call your SignalWire number. You hear the greeting, then your hold music, and the handler logs Queue: enqueue, position 1. Stay on the line for the next section.

If you hear silence instead of music, check that <YOUR_HOLD_MUSIC_URL> is a public audio file. If no Queue: line appears, check that the handler registers its calling.call.queue listener before the call enters the queue.

Connect an agent to a waiting caller via WebSocket (Relay)

Dial the agent, then connect the agent’s call with a queue device that names the queue. SignalWire takes the caller who has waited longest and bridges the two calls. Watch calling.call.connect events on the agent’s call to learn the outcome. The call is placed as in Place a call via WebSocket (Relay).

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as call_agent_relay.py and run: python call_agent_relay.py
import asyncio
from signalwire.relay import RelayClient
from signalwire.relay.event import ConnectEvent
client = RelayClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
contexts=["default"],
)
async def main():
async with client:
call = await client.dial(
devices=[[{
"type": "phone",
"params": {
"from_number": "<YOUR_CALLER_ID>",
"to_number": "<YOUR_AGENT_NUMBER>",
"timeout": 30,
},
}]],
)
async def hang_up_after_playback(_event):
if call.state != "ended":
await call.hangup()
async def on_connect(event: ConnectEvent):
print(f"Connect: {event.connect_state}")
if event.connect_state == "failed":
# The queue was empty.
await call.play(
[{"type": "tts", "params": {"text": "No callers are waiting."}}],
on_completed=hang_up_after_playback,
)
call.on("calling.call.connect", on_connect)
intro = await call.play([{
"type": "tts",
"params": {"text": "Connecting you to the next caller in the dispatch queue."},
}])
await intro.wait()
await call.connect(
devices=[[{"type": "queue", "params": {"queue_name": "dispatch"}}]],
)
await call.wait_for_ended()
asyncio.run(main())

With the caller phone still on hold, run the program and answer the agent phone. After the announcement, the program logs Connect: connecting, then Connect: connected. The caller’s handler logs Queue: dequeue, stops the hold music, and the two phones can talk. Hang up either phone to end both calls.

If the agent hears “No callers are waiting.” and the program logs Connect: failed, the queue was empty: connecting doesn’t wait for a caller to arrive. Check that the caller is still on hold and that both programs use the same queue name, including case.

Take a caller out of a queue via WebSocket (Relay)

Leaving the queue removes the caller and sends a leave event. The call stays up, so your code decides what the caller hears next. In Put a caller in a queue via WebSocket (Relay), the handler leaves the queue when the caller’s two minutes run out, then plays the busy message. Leave the queue the same way to offer a callback, send the caller to voicemail, or move them to another queue.

A caller who hangs up leaves the queue on their own, and also sends a leave event. To remove a caller from a process that doesn’t hold their call, end the call with the REST calling.end command, as in Take a caller out of a queue via SWML.

Track a queue via WebSocket (Relay)

Register a calling.call.queue handler on the caller’s call before the call enters the queue, as the caller’s handler does. The event carries the same fields as the SWML webhooks: its status is enqueue, dequeue, or leave, and it reports the queue’s ID and name, the caller’s position, and the queue’s size. To receive the same events at a webhook instead, pass a status URL when the call enters the queue. The Relay client guide covers event listeners in depth.

Monitor and manage queues via REST

The REST API reads and changes the same queues that SWML and Relay calls use. Calls reach a queue by name, and the REST API addresses it by ID. Use it to build a wallboard, decide which caller to handle next, or rename and clean up queues. The Server SDKs cover each operation:

TaskREST API
Create a queueCreate queue
List queuesList queues
Get one queueGet queue
List the callers waiting in a queueList queue members
Get one waiting callerGet queue member
Rename a queueUpdate queue
Delete an empty queueDelete queue

Get a queue’s size and wait time via REST

This program lists your project’s queues with their IDs, how many callers each holds, and how long those callers have waited on average. Use it to find the ID of a queue that a call created by name:

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as list_queues.py and run: python list_queues.py
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
for queue in client.queues.list()["data"]:
print(
f"{queue['friendly_name']} ({queue['id']}): {queue['current_size']} waiting, "
f"average wait {queue['average_wait_time']} s"
)

To read one queue whose ID you know, use get or Get queue:

GET
/api/relay/rest/queues/:id
curl https://{your_space_name}.signalwire.com/api/relay/rest/queues/id \
-u "<project_id>:<api_token>"
Response
{
"id": "aae131db-214c-46f5-88b6-92004f8467cf",
"project_id": "c6c4679b-716a-456a-9e41-a03821005005",
"friendly_name": "test",
"max_size": 5,
"current_size": 0,
"average_wait_time": 0,
"uri": "/api/relay/rest/queues/aae131db-214c-46f5-88b6-92004f8467cf",
"date_created": "2024-01-15T09:30:00Z",
"date_updated": "2024-01-15T09:30:00Z"
}

friendly_name is the name that calls use to reach the queue, and id is what every REST request about it uses. current_size counts the callers waiting now, and average_wait_time is how long, in seconds, those callers have waited on average: 0 when the queue is empty.

List queues returns your newest queues first, 50 to a page. Follow links.next in the response for the next page, as Paging describes.

List the callers waiting in a queue via REST

This program prints every caller waiting in a queue, next caller first, with their position and how long they’ve waited. The list returns the most recent callers first, 50 to a page, so the program follows links.next through every page, as Paging describes, then sorts by position:

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as list_waiting.py and run: python list_waiting.py
from urllib.parse import parse_qsl, urlsplit
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
members = []
params = {}
while True:
page = client.queues.list_members("<YOUR_QUEUE_ID>", **params)
members.extend(page["data"])
next_url = page.get("links", {}).get("next")
if not next_url:
break
params = dict(parse_qsl(urlsplit(next_url).query))
for member in sorted(members, key=lambda m: m["position"]):
print(f"{member['position']}: waiting {member['wait_time']} s")
GET
/api/relay/rest/queues/:queue_id/members
curl https://{your_space_name}.signalwire.com/api/relay/rest/queues/queue_id/members \
-u "<project_id>:<api_token>"
Response
{
"links": {
"self": "string",
"first": "string",
"next": "string",
"prev": "string"
},
"data": [
{
"call_id": "596e2dea-a269-4765-a0b4-01b82d11c120",
"project_id": "d421473b-d696-449a-a1a1-4ddd83d2d0e5",
"queue_id": "596e2dea-a269-4765-a0b4-01b82d11c120",
"position": 2,
"uri": "/api/relay/rest/queues/596e2dea-a269-4765-a0b4-01b82d11c120/members/596e2dea-a269-4765-a0b4-01b82d11c120",
"wait_time": 172975,
"date_enqueued": "2024-01-15T09:30:00Z"
}
]
}

A caller at position 1 has waited longest and is the next caller an agent’s call connects to. wait_time is how long the caller has waited, in seconds.

A member’s call_id identifies them in this API only: it isn’t the call ID that queue events report or that calling.end takes. To check one member again later, pass the call_id from the list to Get queue member:

GET
/api/relay/rest/queues/:queue_id/members/:id
curl https://{your_space_name}.signalwire.com/api/relay/rest/queues/queue_id/members/id \
-u "<project_id>:<api_token>"

Update or delete a queue via REST

Update a queue to rename it:

# Install: python -m pip install signalwire-sdk==3.4.1
# Save as update_queue.py and run: python update_queue.py
from signalwire.rest import RestClient
client = RestClient(
project="<YOUR_PROJECT_ID>",
token="<YOUR_API_TOKEN>",
host="<YOUR_SPACE>.signalwire.com",
)
queue = client.queues.update("<YOUR_QUEUE_ID>", name="airport-dispatch")
print(queue["friendly_name"])
PUT
/api/relay/rest/queues/:id
curl -X PUT https://{your_space_name}.signalwire.com/api/relay/rest/queues/id \
-H "Content-Type: application/json" \
-u "<project_id>:<api_token>" \
-d '{}'

A new name must be unique among your project’s queues. Renaming doesn’t change the documents and programs that use the old name: the next call that enters or connects to the old name creates a new queue with it.

To delete a queue, replace the last two lines of that program with the line below, or use Delete queue. The queue must be empty: a queue with callers waiting returns 400 Bad Request and stays in place.

client.queues.delete("<YOUR_QUEUE_ID>")
DELETE
/api/relay/rest/queues/:id
curl -X DELETE https://{your_space_name}.signalwire.com/api/relay/rest/queues/id \
-u "<project_id>:<api_token>"