Skip to content
Back to skills

Send Message

BSecurity

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.

  • 75 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
ai-agentstypescriptpythongobashgitapi

Works with

  • cli
  • api

Security analysis

B88/100
  • criticalSends environment variables or credentials to an external URL

Pro shows the line behind each finding and how to fix it

Scanned September 2, 2026

npx -y skills add terrylica/cc-skills --skill send-message --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Send Message?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Send Message
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/terrylica-send-message-cc-skills/badge)](https://www.skillsdirectory.com/skills/terrylica-send-message-cc-skills)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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](../../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

1. **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.
2. **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.
3. **For Tier 3 only**, confirm credentials are present:

```bash
[ -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).

```bash
/usr/bin/env bash << 'WA_EOF'
SCRIPT="$(cc-plugin-root whatsapp-commander)/scripts/wa-cli.ts"

# Tier 1 — build a click-to-chat link (the recommended default; BMP-only body, round-trip verified)
bun "$SCRIPT" link "+1 (555) 123-4567" "Hi {{CONTACT}} — short note and a link: https://example.com/x"

# Tier 1 — body read from a file (multi-paragraph messages are awkward inline)
bun "$SCRIPT" link 15551234567 --file ./whatsapp-message.txt

# Tier 3 — actually send via the Business Cloud API (creds resolved into env first)
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 15551234567 "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 ↔ {{CONTACT}} 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:

  ```bash
  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 ↔ {{CONTACT}} 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 ↔ {{CONTACT}} 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 ↔ {{CONTACT}}) 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:

1. **Did the command succeed?** — If not, fix the instruction or error table that caused it.
2. **Did `wa-cli.ts` flags or output change?** — Update the Usage block to match.
3. **Was a workaround needed?** — Capture it here so the next run doesn't rediscover it.
4. **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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…