| name | send-message |
| description | user wants to send a WhatsApp message, share a link or document via WhatsApp, generate a wa.me click-to-chat link, or message a contact on WhatsApp by phone number. |
| allowed-tools | Bash, Read, Grep, Glob |
Send WhatsApp Message
Reach a contact on WhatsApp. Unlike Telegram (tlg, full MTProto personal send) and
Gmail (gmail-commander, OAuth API send), WhatsApp has no sanctioned personal-account
send API โ Meta only offers programmatic send through the Business Cloud API, and
everything else (WhatsApp Web automation, whatsmeow/Baileys/wppconnect libraries)
is unofficial and risks account bans. This skill therefore defaults to the safe,
zero-credential path and escalates only when the user has set up the Cloud API.
Self-Evolving Skill: This skill improves through use. If instructions are wrong, parameters drifted, or a workaround was needed โ fix this file immediately, don't defer. Only update for real, reproducible issues.
Capability Tiers (pick the lowest that works)
| Tier | Method | Actually sends? | Credentials | When to use |
|---|
| 1 (default) | wa.me click-to-chat link | No โ opens WhatsApp with text pre-filled; the human taps Send | None | Almost always. ToS-safe, instant, works for any number. |
| 2 | WhatsApp Web browser automation | Yes, but brittle | A logged-in WhatsApp Web session (QR) | Not implemented here โ DOM-fragile and ToS-gray. Document, don't build, unless the user insists. |
| 3 | WhatsApp Business Cloud API | Yes, officially | WHATSAPP_TOKEN + WHATSAPP_PHONE_NUMBER_ID (1Password) | The user has a Meta Business app + registered number. See references/business-cloud-api.md. |
Default to Tier 1. Only use Tier 3 when the user has explicitly set up the Business
Cloud API and the credentials resolve (see Preflight).
Preflight
- You need the recipient's phone number in international format (any punctuation is
fine โ the CLI strips non-digits). If you only have an email/username, ask for the
number; WhatsApp is phone-number addressed.
- WhatsApp registration is not verifiable from here โ a
wa.me link still opens even
if the number is not on WhatsApp (it shows an "invalid number" notice on tap). State
this assumption rather than claiming delivery.
- For Tier 3 only, confirm credentials are present:
[ -n "${WHATSAPP_TOKEN:-}" ] && [ -n "${WHATSAPP_PHONE_NUMBER_ID:-}" ] && echo "cloud-api: READY" || echo "cloud-api: NOT CONFIGURED โ use the link tier"
Usage: wa-cli.ts
The CLI is function/enum-driven Bun TypeScript โ run it directly with bun (no build step).
The link command writes the URL to stdout and its preflight/round-trip proof to
stderr (e.g. โ link carries the full 439-char body), so URL=$(bun โฆ link โฆ) still
captures a clean URL. It exits 2 if the body has emoji/astral chars (they would tofu).
/usr/bin/env bash << 'WA_EOF'
SCRIPT="$(cc-plugin-root whatsapp-commander)/scripts/wa-cli.ts"
bun "$SCRIPT" link "+1 (604) 816-8818" "Hi Iris โ short note and a link: https://example.com/x"
bun "$SCRIPT" link 16048168818 --file ./whatsapp-message.txt
export WHATSAPP_TOKEN="$(op item get <ITEM> --vault 'Claude Automation' --fields token --reveal)"
export WHATSAPP_PHONE_NUMBER_ID="$(op item get <ITEM> --vault 'Claude Automation' --fields phone_number_id --reveal)"
bun "$SCRIPT" send 16048168818 "Your appointment is confirmed."
WA_EOF
What to hand back to the user
- Tier 1: present the
wa.me URL as a Markdown link and tell them tapping it opens the
chat with the text pre-filled โ the final Send tap is theirs (WhatsApp does not allow
automating that tap). On desktop they need WhatsApp Web logged in; on phone it deep-links.
- You cannot see the recipient's rendered message on this tier โ there is no feedback
loop. So get it right in one shot: the CLI now runs a preflight (hard-fails on
emoji/astral chars, warns on lone list-number lines / trailing
โโ / over-length) and a
round-trip check, printing โ link carries the full N-char body to stderr. Relay that
N as proof of completeness rather than eyeballing.
- The
api.whatsapp.com/send gray box is a scrollable PREVIEW, not the message. It
truncates visually (a long or list-heavy body "ends at 1"), which triggers false "it got
cut off" reports. Tell the user to click "Continue to WhatsApp Web" / "Open app" โ
the full body drops into the real compose box. Don't assert this blind; the round-trip
โ line is the actual evidence the link is complete.
- Tier 3: the API returns JSON with a
messages[0].id; report the message id as proof
of submission (subject to the 24-hour window / template rules โ see references).
Cloud API Window Rules (Tier 3)
The Business Cloud API does not let you send arbitrary text to anyone at any time:
- Inside the 24-hour customer-service window (the user messaged you in the last 24h):
free-form
type: "text" messages are allowed. wa-cli.ts send sends exactly this.
- Outside the window (first contact / re-engagement): you may send only
pre-approved message templates (
type: "template"), not free text. A free-text send
outside the window returns error 131047. For first contact, prefer Tier 1, or send an
approved template via the raw Graph API (see references).
Message Body Encoding โ keep wa.me bodies emoji-free (BMP only)
Do not put emoji or any other astral-plane (> U+FFFF) character in a Tier-1 wa.me link
body. They arrive on the recipient's device as ๏ฟฝ (U+FFFD replacement char / a
diamond-? tofu) โ even though the source file and the percent-encoded URL are both
perfectly valid UTF-8. The wa.me โ WhatsApp deep-link handoff mangles 4-byte codepoints
(surrogate pairs), while Basic-Multilingual-Plane text โ including all CJK โ survives
untouched. That asymmetry (Chinese renders fine, emoji become ๏ฟฝ) is the diagnostic tell:
it is NOT a source-encoding bug, so don't go re-saving the file as UTF-8 โ the bytes are
already correct.
-
Verified 2026-07-04 (CPC โ Iris zh-Hans report): all Chinese rendered correctly but
๐ โ
โ and keycap sequences (1๏ธโฃ) showed as ๏ฟฝ in WhatsApp. Source bytes were valid
(๐ = f0 9f 91 8b; the URL held %F0%9F%91%8B). Removing the emoji fixed it entirely.
-
Substitute BMP markers: headings ใโฆใ, bullets -, numbering 1) / 2), quotes
ใโฆใ, arrows โ. All โค U+FFFF, render everywhere.
-
Guard before building a link โ fail if any codepoint is astral or an emoji selector:
python3 - "$MSG_FILE" <<'PY'
import sys
t = open(sys.argv[1], encoding="utf-8").read()
bad = sorted({hex(ord(c)) for c in t
if ord(c) > 0xFFFF or ord(c) in (0xFE0F, 0x20E3)
or 0x1F000 <= ord(c) or 0x2600 <= ord(c) <= 0x27BF})
print("ASTRAL/EMOJI PRESENT โ will tofu to โฌ/๏ฟฝ:", bad) if bad else print("BMP-only: safe")
PY
Tier 3 Cloud API send transmits real UTF-8 in the POST body and does not have this
problem โ the mangling is specific to the Tier-1 click-to-chat deep link.
The link command now enforces the emoji check itself (hard error, exit 2) and prints a
round-trip โ link carries the full N-char body line โ so you rarely need to run the guard
above by hand, but it's kept for quick pre-checks on a draft file.
Keep Tier-1 bodies short and structurally flat
The landing-page preview is a fixed, scrollable box. Long or list-heavy bodies scroll out
of view and look truncated, which reads as a bug to the recipient/operator even though the
link carries everything. Two of the three round-trips in the CPC โ Iris case (2026-07-04) were
this false alarm, not real defects. So, for Tier-1:
- Keep it concise (the CLI warns past ~700 chars). WhatsApp messages are read on phones;
a tight 300โ450-char note beats a wall of text.
- Inline the asks as
(1) โฆ (2) โฆ inside a paragraph โ do NOT start a line with a bare
1) / 1.. A line that is just 1 is the classic "message got cut at 1" preview artifact.
- Never end a line with
โโ flowing into a numbered item โ it renders as a dangling
-- 1. Use a colon lead-in (โฆ้ฎไธคไปถไบ๏ผ) instead.
- The CLI's preflight warns on all three; treat the warnings as "fix before you hand it over".
Anti-Patterns (NEVER DO)
| Anti-Pattern | Why It Fails |
|---|
Claiming a wa.me link "sent" the message | It only pre-fills; the human taps Send. Say so. |
Using an unofficial library (Baileys, whatsmeow, wppconnect) to send as a personal account | Violates WhatsApp ToS; risks number ban. Not part of this skill. |
| Passing a number without a country code | wa.me needs full E.164; the CLI rejects <8 digits. |
| Free-text Cloud API send to a cold contact | Outside the 24h window only approved templates send (error 131047). |
| Hardcoding the Cloud API token in a script/commit | Resolve from 1Password into env at call time (mirrors gmail-commander/tlg). |
Emoji / astral-plane chars (๐โ
โ, keycaps) in a wa.me link body | Arrive as ๏ฟฝ (U+FFFD) โ the deep-link handoff drops 4-byte codepoints; BMP/CJK survive. Keep Tier-1 bodies BMP-only. |
Error Handling
| Symptom | Cause | Fix |
|---|
CLI: too few digits | Number missing country code | Pass full international format |
CLI: Cloud API send needs WHATSAPP_TOKENโฆ | Tier 3 creds absent | Use Tier 1 link, or resolve creds via 1Password |
API Cloud API HTTP 401 | Token expired/invalid | Rotate the token in Meta โ update 1Password |
API error 131047 | Outside 24h window | Send an approved template, or use Tier 1 |
wa.me opens but shows "invalid number" | Recipient not on WhatsApp / wrong number | Confirm the number with the user |
Recipient sees ๏ฟฝ / diamond-? where emoji were | Astral-plane emoji lost in the wa.meโWhatsApp deep-link decode (source UTF-8 fine; CJK survives) | Strip emoji from the body; use BMP markers (ใใ, -, 1)). See "Message Body Encoding". |
| User reports the message "ends at 1" / looks cut off | The api.whatsapp.com/send gray box is a scrollable preview, not the message; long/list-y bodies scroll out of view | Not a bug โ the round-trip โ carries N chars proves completeness. Tell them to click "Continue to WhatsApp Web"; keep bodies short & inline (1)(2). |
Evolution Log
-
2026-07-04 (later) โ hardened the Tier-1 path against the two failure modes above. Added
to wa-cli.ts link: (1) an emoji/astral preflight (hard error, exit 2) so a tofu body is
never emitted; (2) structural warnings for lone list-number lines / trailing โโ /
over-length that make the preview look truncated; (3) a round-trip check that decodes
the built URL and asserts it equals the input, printing โ link carries the full N-char body. Lesson baked in: on Tier-1 the agent has no view of the rendered result, so verify
what's verifiable (round-trip) and never assert preview behavior blind. Cost that motivated
this: one CPC โ Iris message took three user round-trips (emoji โ dangling dash โ false
"truncated" preview) โ all now caught before hand-off.
-
2026-07-04 โ emoji tofu on the Tier-1 link path. A zh-Hans report (CPC โ Iris) rendered
all Chinese correctly but showed ๐ โ
โ / keycaps as ๏ฟฝ in WhatsApp. Root cause: the
wa.me โ WhatsApp deep-link handoff drops astral-plane (4-byte) codepoints while BMP text,
including CJK, survives โ the source file and percent-encoded URL were both valid UTF-8.
Fix + guard documented in the new "Message Body Encoding" section: keep Tier-1 bodies BMP-only.
-
2026-06-22 โ initial skill. WhatsApp has no personal-account send API. Modelled on
tlg/gmail-commander conventions but split into capability tiers because the platform
forbids the equivalent of MTProto/OAuth personal send. Default = wa.me link (the path
proven in the VanJobbers โ CPC engagement); Cloud API send wired but credential-gated.
CLI written as function/enum-driven Bun TypeScript per the repo language preference.
Post-Execution Reflection
After this skill completes, check before closing:
- Did the command succeed? โ If not, fix the instruction or error table that caused it.
- Did
wa-cli.ts flags or output change? โ Update the Usage block to match.
- Was a workaround needed? โ Capture it here so the next run doesn't rediscover it.
- New convention learned about WhatsApp's limits? โ Add it to the tier table / window rules.
Only update if the issue is real and reproducible โ not speculative.