| name | whatsapp |
| description | WhatsApp messages, contacts, groups, and live voice calls (place/answer, talk in your own voice; not generic text/SMS). Use to message or call someone on WhatsApp. |
WhatsApp (CLI: whatsapp)
You never stop or restart anything, and you don't manage the daemon by hand. The
whatsapp CLI runs its own background daemon: the restart skill runs whatsapp start at boot to bring it up (so inbound WhatsApp notifications flow before you
send anything), and every command below also brings it up on demand. Your whole
world is four verbs: connect, status, send, messages (plus profile and calls).
The one rule
If whatsapp status shows linked: false, run whatsapp connect, except when
it also shows connecting: true. In that case a QR page is already active:
wait for the user to scan it and do not start another connect. This is the only
recovery you ever need. Never re-link, re-pair, or "restart the daemon" any other
way; connect is idempotent outside an active QR session.
Set up (one command)
whatsapp connect is the single setup verb. It picks the right path for the box on
its own, so you never choose between modes. Every output carries a next: step, so
just do what it says:
- Hosted (vesta.run) box: compose a warm, natural first-contact opener in your
own voice that says hi and introduces you by name, for example, "Hi, it's Nova,
nice to meet you!" Then run
whatsapp connect --opener '<your opener>'. The
opener is prefilled for the USER to send TO you, so phrase it as the user reaching
out. The command claims the agent's own managed number and links
it, returning {status:"linked", number, next:...}. Follow next: share the
number and its wa.me link so the user messages you FIRST (reply-first: never
cold-initiate), then reply only once they do.
- Still filling:
{status:"provisioning", next:...} means the number is still
being set up. Re-run whatsapp connect in about 30 seconds; repeating is safe.
- Blocked:
{status:"blocked", next:...} means that number was banned. Re-run
whatsapp connect to get a fresh one.
- Rate limited:
{status:"rate_limited", reason, next:...} means the number keeps
failing to link and the ban guard paused pairing. Wait out the cooldown named in
reason before running whatsapp connect again; retrying sooner is what gets a
number banned.
- Self-hosted (user's own WhatsApp): it serves a QR page and returns
{status:"linking", url, next:...}. Send the user the URL to scan in WhatsApp >
Settings > Linked Devices. The command returns as soon as the live page is ready.
Wait for the user to say they scanned it, then run whatsapp status once. Never
choose a port, register a QR service, or run a second connect while the page is
active. If the user cannot scan a QR, and
explicitly approves a pairing attempt, run whatsapp connect --phone '+E.164'
with the exact account number. Confirm the echoed number, then send the returned
code for WhatsApp > Linked Devices > Link a Device > Link with phone number.
While that code is active, wait for the user and do not run connect again. See
SETUP.md / MANAGED_AUTH.md.
Other ways to connect
- User owns the number (
whatsapp connect --own-number): the pool hands you a
fresh number and its verification code to relay; the USER registers that number on
their OWN phone and owns it, and you link only as a companion. Use this only when the
user wants the WhatsApp account to live on their own device: they keep their phone
online and re-link it themselves if it drops. Normal messaging applies (no reply-first
restriction, since the number is theirs, not a fresh pooled one).
- Direct token (no vesta.run needed): the doubletick number service is a standalone API, not
only reachable through vesta.run. If the box operator set
DOUBLETICK_API_URL and
DOUBLETICK_API_KEY (a per-account wak_ token they minted for you), whatsapp connect uses them automatically: the same managed number and pairing, straight to the
home box, no membership gate and no control plane in the loop. You do nothing
different, it is transparent. This is how a non-managed agent gets a pooled number
when the operator has handed them a token.
Before the companion connects, egress selection is WHATSAPP_PROXY_URL, then a
doubletick residential lease, then direct. If you're managed or on a datacenter IP,
the skill leases a residential proxy from doubletick automatically; it hardfails
rather than run WhatsApp over a datacenter IP. A supplied proxy is validated too.
Check state
whatsapp status is your one diagnostic:
- linked:
{"linked":true,"number":"+44...","connected":true}
- connecting:
{"linked":false,"connecting":true,"method":"qr","next":"wait for the user to scan..."}
- pairing code active:
{"linked":false,"connecting":true,"method":"phone","next":"wait for the user to enter..."}
- not linked:
{"linked":false,"connected":false,"next":"run: whatsapp connect","reason":"<why>"}
Send
- Form:
whatsapp <subcommand> [positionals] [--flag value ...]. Subcommand goes first, before any flags.
- Most common subcommands accept leading positional args that the CLI rewrites into flags (e.g.
whatsapp send 'Alice' 'Hi' is identical to whatsapp send --to 'Alice' --message 'Hi'). You can always use the flag form.
- Flags for a specific subcommand:
whatsapp <subcommand> --help. The top-level whatsapp with no args prints the command list.
- Names for
--to / --chat-id / --group: contact name, phone (+E.164), group name, or JID; the CLI resolves them.
- Send every message body through
--message - and a quoted heredoc, as in the example below. The shell expands nothing inside <<'MSG', so apostrophes, quotes, backticks, $(...) and newlines all pass through untouched. An inline --message 'text' breaks on the first apostrophe, so use it only for text you can see has none.
send-message enforces short-bubble texting: a wall (over ~220 chars, or any text after a full stop) is rejected so you re-send as several short calls, one thought each. Don't use full stops at all: a ., ! or ? may only close a bubble, never carry text after it. Ellipses stay free, they're a beat rather than a stop. For genuine reference material the user asked for (a brief, a code block, a list), pass --longform to bypass. This applies to heredoc sends too, so --longform is the only escape hatch.
- A numbered or bulleted list is fine to send as one message (each item is one short thought); a line-leading marker like
1. or 2) is not a full stop, so a list does not need --longform.
whatsapp send --to 'Alice' --message - <<'MSG'
can't wait to see you
MSG
whatsapp send --to 'Alice' --message - --reply-to '<message_id>' <<'MSG' # quote a message
same here
MSG
--to accepts a contact name, phone (+E.164), group name, or JID; the CLI resolves it.
--message - reads the body from stdin; pair it with a quoted heredoc so the shell cannot
mangle or evaluate anything in the text.
- Short bubbles only: a wall (over ~220 chars, or 3+ sentences in one bubble) is rejected.
Re-send as several short calls, one thought each. Pass
--longform only for genuine
reference material the user asked for (a brief, a code block, a list).
- Before texting an unknown raw number, save it first with
add-contact (name + phone).
Read
whatsapp messages [--to <name>] [--query <text>] [--after <RFC3339>] [--limit N] reads the local DB.
whatsapp chats, whatsapp contacts, whatsapp groups list the obvious things.
whatsapp backfill --to <name> asks the phone for older history when the local DB is thin.
whatsapp check-delivery --message-id <id> (or --recent) checks whether a send landed.
- Message IDs come from inbound notification payloads (
message_id) or messages output.
Profile
Change the agent's own WhatsApp name/picture from its own client (no phone, no QR, works while linked):
whatsapp profile name 'mozzy' sets the display (push) name. Account-wide and immediate, but a
contact keeps seeing the OLD name until you next message them, so message them once to refresh it.
whatsapp profile photo ~/avatar.jpg sets the picture. JPEG (PNG is auto-converted), roughly square (~640x640).
Edited and deleted messages
People change their minds after they hit send, so a message you already read can change or vanish:
- An edit arrives as an
edit notification whose body carries what the message says now, just like a plain message, naming the message that changed (target_message_id) and the text you last saw (old_text). The stored message is rewritten, so list-messages and search show only the new text. Answer again only if the edit asks something new: a fixed typo needs nothing from you.
- A deletion (delete-for-everyone) arrives as a
revoke notification with the text you last saw in old_text. They took it back, so treat it as unsaid and do not quote it at them.
Voice calls
Hold a live call in your own voice (the voice skill's TTS) and hear the other person (its STT):
- The other person's speech arrives as
call_utterance notifications; it interrupts like any message,
so you answer live by speaking with whatsapp say '<one short line>' (one spoken thought at a
time, not a monologue). A newer say replaces whatever is still playing.
whatsapp call --to <name> places a call and returns once answered; greet them with say.
Inbound calls answer automatically. whatsapp hangup ends it; whatsapp call-status reports the active call.
- Requires the
voice skill with both STT and TTS; without it, calls are declined and you are told to set it up.
- Calling is your loudest, most interrupting reach. Reserve
whatsapp call for the genuinely
time-critical (a real deadline, a safety or money issue, something they asked to be called about)
and only after messages went unanswered. Respect anything the constitution says about calling.
More commands
whatsapp with no args lists everything. Others (all take --help): send-file, send-audio,
react, revoke-message, download-media, create-group, leave-group, rename-group,
set-group-description, set-group-photo, get-group-invite-link, update-group-participants,
archive-chat, delete-chat, remove-contact.
Rules
- Send one WhatsApp call at a time. Never batch sends (or
say lines) in a parallel tool-call
block: if one fails you can't tell which landed, and a retry sends a duplicate; parallel say lines race.
- Never re-link without the user's explicit go-ahead. Pairing is rate-limited because repeated
attempts get numbers flagged and banned. If linking fails, report it and wait; don't retry-loop.
- Phone numbers are E.164 with a leading
+ (e.g. +12025551234). Auth state lives in ~/.whatsapp/.
Advanced setup (second/personal account, read-only, silent instances) and how to develop the CLI:
see SETUP.md and DEVELOPING.md.
Contact Preferences
[How the user prefers to communicate with different contacts]