Skip to main content
This guide shows how to create a WhatsApp assistant with Timbal that can:
  • Respond to messages in real time via webhooks
  • Persist conversations using Timbal Knowledge Bases or JSONL
  • Handle text messages and preserve metadata for audio, images, and documents
The runnable example answers text messages. Media IDs and filenames are placeholders until you add downloading and transcription or file analysis. Install timbal[server] and httpx; add pyngrok only if you use the optional tunnel helper. This demonstration processes the agent inside the webhook request. For production, authenticate webhooks, deduplicate message IDs, acknowledge promptly, and process through a durable queue.

Setup Requirements

1

Create or open your WhatsApp App

Go to the Meta console and open your app’s WhatsApp section: Meta WhatsApp App Dashboard.
2

Set Callback URL and Verify Token

  1. Set the Callback URL to your endpoint, e.g., https://yourdomain.com/
  2. Set a Verify Token (keep it secret)
  3. In your app code, use the same token (see WHATSAPP_VERIFY_TOKEN in the example below)
  4. Click Verify and Save
3

Subscribe to fields

In the WhatsApp product settings, subscribe to message-related events (e.g., messages, statuses).
Create a .env file and set:
.env
The main example stores JSONL. The optional KB section replaces that storage; setting environment variables alone does not switch it. Replace your_supported_version with your app’s Graph API version.

Minimal Webhook App (FastAPI)

Use a simple FastAPI app to receive the webhook and pass the full payload to the agent. The agent will process everything in pre_hook and send responses in post_hook.
app.py
Expose locally (optional):
Then set the ngrok HTTPS URL as the Callback URL in the Meta console.

Creating the Agent

agent.py
The pre_hook runs before invoking the agent. It extracts metadata from the webhook, builds a prompt from the incoming message, persists the inbound event, and sets values in span.input for the agent to use during inference.
agent.py
The post_hook runs after obtaining the agent’s response. It sends the text via WhatsApp and persists the outbound event to JSONL.
agent.py

Optional: system prompt example

Optional: Timbal KB persistence and history

If you prefer Timbal Knowledge Bases instead of JSONL, use the async SQL helper below and replace the storage calls in async pre/post hooks. Create the whatsapp_messages table with the listed columns first. The helper targets K2; pass legacy=True for a legacy KB. User values are SQL parameters.

Media Handling Notes

  • Images: The webhook includes an image id. Use the Graph API to fetch the media URL or content using your WHATSAPP_ACCESS_TOKEN. Provide the URL or a File object to the agent if needed.
  • Audio: The webhook includes an audio id. You can fetch and transcribe audio, then pass the transcript as the prompt.
  • Documents: You receive basic metadata (e.g., filename). Decide whether to fetch and analyze content or treat as a reference.
Keep the pre_hook minimal: extract the message type, create a concise prompt, and persist the event.

Complete implementation

For additional webhook and utility examples, see the Timbal repository. These are separate examples and may use different configuration from this guide:

Possible Improvements

Each WhatsApp webhook starts a separate agent run. Persist every inbound message immediately (JSONL or KB), then wait a short debounce window. After that window, if storage shows a newer message for the same user, bail this run so the most recent run answers with full context. No in-memory buffer is required.
  • Why it helps: avoids replying to partial context when the user is still typing or sending media.
  • Trade-offs: adds small latency; choose a short window (e.g., 800–1500 ms).
  • Example approach (JSONL):
Use the WhatsApp Cloud API to mark messages as read. This improves UX and clears unread indicators for the user.
Call mark_as_read(message_id) right after parsing the webhook.
You can mark the message as read and briefly show a typing indicator while the agent thinks or uses tools. Optionally send a short acknowledgement, then follow up with the final answer.This request follows Meta’s typing indicator example. The synchronous helper below should run in a worker, rather than block an async webhook handler.
  • Call this right after parsing the webhook and before long operations.
  • Keep interim notifications minimal to avoid noise.
Persist metadata for images, audio, videos, and documents so the agent can reuse them as context later (e.g., to re-attach an image or analyze it again).Suggested fields to store: id, user_phone, direction, message_type, media_id, media_url (or object store key), mime_type, caption, timestamp.
Later, query recent media by user_phone or conversation_id and inject URLs or file handles into the agent context as needed.

Running

Expose with ngrok, then verify the webhook in the Meta console. New messages should invoke your agent and send responses via WhatsApp.

Troubleshooting

  • Verify token: Ensure the value in Meta matches VERIFY_TOKEN in your app
  • Public URL: Your callback must be publicly reachable (use ngrok)
  • HTTPS: Required in production
  • Tokens: Confirm WHATSAPP_ACCESS_TOKEN and WHATSAPP_PHONE_NUMBER_ID are set
  • Phone ID: Ensure your app uses the correct phone number ID
  • Permissions: Check token permissions in Access Token Debugger
  • Check ORG_ID and KB_ID if using Timbal KB
  • If using JSONL, verify the JSONL file path and write permissions