A Dispatch channel plugin for WhatsApp, using the native Rust whatsapp-rust client.
No Docker. No Meta Business Account. No Cloud API app. No external daemon. The plugin is a single Rust binary that links a WhatsApp Web session via QR code and stores that session locally in SQLite.
This repository is the source of truth for the first-party WhatsApp channel plugin. The main dispatch-plugins repository keeps only a pointer README plus catalog metadata so WhatsApp remains discoverable without carrying its dependency graph inside the standard plugin workspace.
Implemented:
capabilitiesconfigurehealthpoll_ingress(one-shot receive)start_ingress(background receive worker, emitschannel.event)stop_ingressdeliver(text plus one optionaldata_base64attachment)push(same send path asdeliver)status(mapped to WhatsApp typing indicators)shutdown
Not yet implemented (v0.1.1 follow-ups):
- inbound attachment byte download (metadata only in v0.1.0)
- multiple outbound attachments in one Dispatch message
- group send and group receive as first-class behavior
- reactions, edits, read receipts as distinct event types
- phone-number-to-JID resolution
Build from source. The whatsapp-rust dependency stack requires a working protoc at build time on every platform. Install it via brew install protobuf on macOS, your distribution's package manager on Linux, or choco install protoc on Windows. The choco example assumes Chocolatey is already installed; otherwise, install Chocolatey first or place protoc.exe on PATH manually. The resulting channel-whatsapp binary has no runtime dependencies beyond TLS root certificates.
cargo build --releaseThe binary is written to target/release/channel-whatsapp.
Before the plugin can send or receive, it must be linked to a WhatsApp account. Run the built-in --link subcommand and scan the displayed QR code from the WhatsApp app on your phone (WhatsApp -> Settings -> Linked Devices -> Link a Device):
channel-whatsapp --link --device-name "Dispatch"Flags:
-n, --device-name <NAME> Device name shown in WhatsApp's linked
devices list. Defaults to `Dispatch`.
--account <NAME> Logical account name; selects the
per-account subdirectory under the
default store root. Defaults to
`default`. Use this to link multiple
WhatsApp accounts on the same host.
--sqlite-store-path <PATH> Absolute path to the SQLite store.
Overrides --account.
-h, --help Print help and exit.
On success the plugin prints a JSON summary containing the linked JIDs, device id, push name, and store path. Subsequent runs with no arguments reuse the stored session.
| Field | Purpose |
|---|---|
sqlite_store_path |
Absolute path to the SQLite store. Overrides account. |
account |
Logical account name selecting the per-account subdirectory when sqlite_store_path is unset. Defaults to default. |
default_recipient |
Fallback JID for operator-driven push, deliver, and status frames with no routing metadata. |
poll_timeout_secs |
Receive timeout in seconds for one poll_ingress cycle. Defaults to 10. |
Default store layout:
$XDG_CONFIG_HOME/dispatch/channels/whatsapp/<account>/store.db
# or
$HOME/.config/dispatch/channels/whatsapp/<account>/store.db
Outbound delivery and status frames currently require a full WhatsApp JID:
15551234567@s.whatsapp.netfor a direct-message chat<lid>@lidfor LID-addressed direct-message chats
Bare phone numbers are rejected. Group JIDs (@g.us) are not supported in v0.1.0.
The intended operator flow is to receive an inbound event first, then reuse conversation.id as the outbound message.metadata.conversation_id.
dispatch channel call channel-whatsapp \
--request-json '{"kind":"health","config":{}}'
dispatch channel poll channel-whatsapp \
--config-file ./whatsapp.toml --once
dispatch channel call channel-whatsapp \
--request-json '{
"kind": "push",
"config": {},
"message": {
"content": "Hello from Dispatch",
"metadata": {
"conversation_id": "15551234567@s.whatsapp.net"
}
}
}'The plugin transport is JSON-RPC 2.0 over JSONL on stdio. Operators normally go through the Dispatch CLI rather than writing raw envelopes.
poll_ingressopens a WhatsApp session, drains queued inbound messages until the idle window or timeout is reached, then exits. This is the one-shot path used bydispatch channel poll --once.start_ingress/stop_ingressrun a background receive worker on a dedicated OS thread with its own current-thread tokio runtime. Inbound messages are sent back to the host aschannel.eventnotifications between JSON-RPC responses.
Inbound media is surfaced as attachment metadata:
message.attachments[].kind=image,video,audio, ordocumentmime_type,size_bytes, andnameare populated when WhatsApp provides them- dimensions, caption, duration, and audio
pttstate are carried inattachments[].extras - attachment bytes are not downloaded in v0.1.0
If an inbound message carries only media and no text or caption, the plugin emits a fallback body like (1 attachment(s)) so the event is still visible to the agent.
deliver and push support one optional inline attachment per message. Only data_base64 attachments are supported in v0.1.0.
urlattachments are rejectedstorage_keyattachments are rejected- image, video, audio, and document payloads are inferred from
mime_type - document attachments use the attachment
nameas the WhatsApp file name - audio attachments cannot carry
message.content; send the text as a separate message or send the file as a document attachment instead
Example:
{
"kind": "deliver",
"config": {},
"message": {
"content": "here is the file",
"metadata": {
"conversation_id": "15551234567@s.whatsapp.net"
},
"attachments": [
{
"name": "report.pdf",
"mime_type": "application/pdf",
"data_base64": "..."
}
]
}
}Dispatch emits status frames to mark turn-taking. The plugin maps the subset that makes sense for WhatsApp chatstate updates:
| StatusKind | WhatsApp action |
|---|---|
processing, delivering, operation_started |
composing |
completed, cancelled, operation_finished |
paused |
info, approval_needed, auth_required, others |
accepted, no upstream traffic |
The status frame's conversation_id is required and is treated the same way as an outbound recipient JID.
- Multiple outbound attachments in one Dispatch message are not supported in v0.1.0.
- Group JIDs are intentionally out of scope for the first release.
- Inbound attachment bytes are not downloaded yet; only metadata is surfaced.
- Reactions, edits, and read receipts are ignored for now.
- The SQLite store contains the linked WhatsApp session and should be treated like a credential.
- Restrict the store directory permissions, for example:
chmod 700 ~/.config/dispatch/channels/whatsapp/<account>/- Removing the linked device from WhatsApp on the phone immediately revokes this plugin's access.
MIT