π₯ Listeners#
When your bot needs more than a single reply β for example, asking a follow-up question or collecting a sequence of inputs β listeners let you pause execution and wait for the userβs next message, right inside the same handler function. No extra handler needed.
Warning
Limitations and Resource Safety Warnings:
Multi-worker environments: Listening is not supported when running the server with multiple workers (e.g.,
pywa run --workers > 1), as workers do not share in-memory listener states.Memory Leak Risk: Listening without a
timeoutis highly discouraged. If the user never responds, the listener remains in memory indefinitely. Always specify a reasonabletimeout.Thread Pool Exhaustion (Synchronous Clients): In synchronous pywa (not
pywa_async, each active listener blocks a worker thread. If you run pywa synchronously with ASGI frameworks like FastAPI or Starlette, active listeners can quickly exhaust the AnyIO thread pool (default is 40). If the limit is reached, your server will freeze and drop incoming webhooks.Actionable mitigations:
(Recommended) Migrate to the asynchronous client (
pywa_async) for fully non-blocking listeners.Enforce strict, shorter
timeoutdurations on all listeners to free up threads faster.Increase the AnyIO thread limit by adjusting
pywa.server.ANYIO_THREADS_LIMITto a higher value.
Listening#
In this example, we wait for the user to send their age. The listener checks that the reply is a text message containing only digits, then branches based on the value.
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(client: WhatsApp, msg: types.Message):
7 sent = msg.reply("Hello! How old are you?")
8 age_reply: types.Message = sent.wait_for_reply(
9 filters=filters.text & filters.new(lambda _, m: m.text.isdigit())
10 )
11 age = int(age_reply.text)
12 if age < 18:
13 age_reply.reply("You are too young to use this service.")
14 else:
15 age_reply.reply("Welcome! You can now use the service.")
wait_for_reply() blocks execution until the user sends a message that
matches the filter β filters.text & filters.new(lambda _, m: m.text.isdigit()) here.
The matching update is returned as a Message object, so you can read
age_reply.text, reply to it, or pass it along to other logic.
Canceling#
Listeners block until a matching message arrives. You can also give the user a way out by
providing cancelers (e.g., a cancel button) or a timeout in seconds.
If either triggers, a exception is raised β see Handling cancel and timeout below.
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(_: WhatsApp, msg: types.Message):
7 sent = msg.reply("Hello! How old are you?", buttons=[types.Button(title="Cancel", callback_data="cancel")])
8 age_reply = sent.wait_for_reply(
9 filters=filters.text & filters.new(lambda _, m: m.text.isdigit()),
10 cancelers=filters.callback_button & filters.matches("cancel"),
11 timeout=60,
12 )
13 ...
Handling cancel and timeout#
When a listener is canceled or times out, pywa raises ListenerCanceled
or ListenerTimeout respectively. Catch them to send a helpful response.
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(_: WhatsApp, msg: types.Message):
7 try:
8 age_reply = msg.reply(
9 text="Hello! How old are you?",
10 buttons=[types.Button(title="Cancel", callback_data="cancel")],
11 ).wait_for_reply(
12 filters=filters.text & filters.new(lambda _, m: m.text.isdigit()),
13 cancelers=filters.callback_button & filters.matches("cancel"),
14 timeout=60,
15 )
16 except types.ListenerCanceled:
17 msg.reply("You canceled the operation by clicking the cancel button.")
18 return
19 except types.ListenerTimeout:
20 msg.reply("You took too long to respond. Please try again later.")
21 return
22 ...
Custom listeners#
For advanced use cases, you can use the lower-level WhatsApp.listen() method directly.
It lets you specify which sender and update type to wait for, and is what all the shortcuts
(wait_for_reply, wait_for_click, etc.) are built on top of.
Attention
If the listener did not use the update (the update did not match the filters or the cancelers), the update will be passed to the handlers. This means that the update can be processed by other handlers registered for the same update type.
If you must prevent the update from being passed to the handlers, call
stop_handling() on the update inside the filters
or cancelers (this only affects handler dispatch, not the listener itself).
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("confirm"))
6def confirm_action(_: WhatsApp, msg: types.Message):
7 try:
8 confirmation: types.Message = wa.listen(
9 to=msg.sender,
10 filters=filters.message & filters.matches("yes", "no"),
11 cancelers=filters.message & filters.matches("cancel"),
12 timeout=30,
13 )
14 if confirmation.text == "yes":
15 confirmation.reply("β
Confirmed!")
16 else:
17 confirmation.reply("β Didn't confirm")
18 except types.ListenerCanceled:
19 msg.reply("You canceled the operation by clicking the cancel button.")
20 return
21 except types.ListenerTimeout:
22 msg.reply("You took too long to respond. Please try again later.")
23 return
Shortcuts#
PyWa provides several shortcuts to create listeners directly from sent messages:
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(client: WhatsApp, msg: types.Message):
7 age = msg.reply("Hello! How old are you?").wait_for_reply(filters.text)
8 msg.reply(f"You are {age.text} years old")
1from pywa import WhatsApp, types, filters
2
3wa = WhatsApp(...)
4
5@wa.on_message(filters.command("start"))
6def start(client: WhatsApp, msg: types.Message):
7 msg.reply(f"Hello {msg.from_user.name}!").wait_until_delivered()
8 msg.reply("How can I help you?")
Other shortcuts include wait_for_click(),
wait_for_selection(),
wait_until_read(),
wait_until_played(),
wait_for_location(),
wait_for_contact_info() and more.
- Listeners reference
SentMessage.wait_for_reply()SentMessage.wait_for_click()SentMessage.wait_for_selection()SentMessage.wait_until_read()SentMessage.wait_until_delivered()SentMessage.wait_for_completion()SentMessage.wait_for_call_permission()SentMessage.wait_for_incoming_voice_call()SentVoiceMessage.wait_until_played()SentLocationRequest.wait_for_location()SentContactInfoRequest.wait_for_contact_info()CreatedTemplate.wait_until_approved()ListenerTimeoutListenerCanceledListenerStopped