← All Skills

snappy-whatsapp

v1.0.0
15 files, 106.9 KB ~3,547 words · 15 min read Updated 2026-09-09

snappy-whatsapp skill

52 of 54 checks pass
5 eval scenarios, 2 held out
What it can do
mark-read message-idwrite
media to url caption?send
notify textsend
read limit?read
list limit?read
thread from limit?read
send to messagesend
What does not pass yet
Architecture 2 endpoints
🔑 Bearer token auth
graph.facebook.com2 endpoints
POST/v21.0/$WHATSAPP_PHONE_ID/messages
GET/v21.0/$WHATSAPP_PHONE_ID/messages
$ npx snappy-skills install snappy-whatsapp
zip ↓
File Tree
├── AGENTS.md ├── SKILL.md ├── adapter.ts ├── api.ts ├── evals.json ├── face.test.ts ├── faces/ │ ├── components/ │ │ └── whatsapp-chat.tsx │ ├── family.tsx │ └── fixtures/ │ ├── whatsapp-compose.json │ ├── whatsapp-decision.json │ ├── whatsapp-list.json │ ├── whatsapp-message.json │ └── whatsapp-thread.json ├── templates.md └── workflows.md
Documents
AGENTS.md

snappy-whatsapp -- Agent Loader#

You handle Snappy's WhatsApp channel: informal client comms, check-ins, meeting reminders, invoice follow-ups, onboarding welcomes, media sends, and Robert self-notifications. Direct WhatsApp Cloud API (Meta Business API) -- no Xano middleware.

API module#

typescriptimport { sendMessage, sendMedia, notifyRobert, getRecentWhatsAppMessages } from "../snappy-whatsapp/api.ts";

Or CLI:

bashnpx tsx ~/.claude/skills/snappy-whatsapp/api.ts send "+14155551212" "Hey, quick update..."
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts media "+14155551212" "https://example.com/img.png" "Caption"
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts notify "Build completed"

Credentials loaded via snappy-settings/load.ts from .env.cache.

Required env vars: WHATSAPP_TOKEN, WHATSAPP_PHONE_ID, ROBERT_PHONE


API (Meta WhatsApp Cloud API)#

Base URL: https://graph.facebook.com/v21.0/{WHATSAPP_PHONE_ID}/messages

Auth: Bearer $WHATSAPP_TOKEN

All requests include "messaging_product": "whatsapp" in the body.

Copy-paste patterns#

bash# Send text
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d '{"messaging_product":"whatsapp","to":"+14155551212","type":"text","text":{"body":"Hey, quick update..."}}'

# Send image
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d '{"messaging_product":"whatsapp","to":"+14155551212","type":"image","image":{"link":"https://example.com/img.png","caption":"Dashboard screenshot"}}'

# Notify Robert (same as send text, using $ROBERT_PHONE)
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d "{\"messaging_product\":\"whatsapp\",\"to\":\"$ROBERT_PHONE\",\"type\":\"text\",\"text\":{\"body\":\"Build completed\"}}"

Rules#

  • Phone numbers MUST be E.164 (+14155551212) -- Meta rejects anything else
  • sendMessage(to, message) wraps as { type: "text", text: { body: message } } -- the Cloud API field is text.body
  • sendMedia(to, url, caption) sends { type: "image", image: { link, caption } } -- media URL must be publicly accessible HTTPS
  • Always include caption with media sends
  • One complete thought per message -- no rapid-fire multiple messages
  • Mon-Fri 9am-7pm in recipient's timezone unless urgent
  • Look up phone from snappy-clients or snappy-knowledge -- never hardcode

The draft never arrives alone#

--json on send is a PREVIEW and touches nothing — no send, no staged row. It

prints the message inside the conversation it joins:

bashnpx tsx ~/.claude/skills/snappy-whatsapp/api.ts send "+14155551212" "Thursday 9am works." --json
# {kind, thread, threadKind, threadTotal, draft:{to, body}, doors:[Send, Later]}
  • thread is the SAME rows thread --json prints for that correspondent. Show

it to the person before asking them to approve anything; a draft with no

conversation under it asks them to trust your summary of the conversation.

  • kind says which of two situations this is: whatsapp-decision when a

conversation is in hand, whatsapp-compose when there is none. The Cloud API

cannot be polled — the only inbound is the local webhook log — so a first

message to someone honestly has thread: []. Never present that as a reply.

  • E.164 and the log's bare digits are matched on digits, so +1 415 555 1212

finds the chat the webhook filed as 14155551212.

  • WITHOUT --json, send is unchanged: it stages for the owner's decision.

Channel priority (when to use WhatsApp)#

Priority Channel Best for
1 Slack Active clients with shared channels
2 Email Formal, async, paper trail
3 WhatsApp Quick, personal, informal
4 iMessage Apple-to-Apple personal contacts
5 Telegram Robert self-notifications

Uses#

skill relationship
snappy-clients Source of client phone, project status, comm preferences
snappy-calendar Meeting data feeds reminder workflows
snappy-freshbooks Overdue invoices feed nudge workflows
snappy-ops Orchestrator -- flags stale clients, tomorrow's meetings
snappy-slack Sibling -- primary client comms; WhatsApp adds personal touch

Full skill reference#

If this loader is insufficient, load ~/.claude/skills/snappy-whatsapp/SKILL.md as last resort. Templates: templates.md. Workflows: workflows.md.

If this loader doesn't cover your case:

bashecho "[$(date -u +%FT%TZ)] snappy-whatsapp: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log

Show produced work with snappy-faces: call draw for image channels or lang for MCP Apps.

<!-- SKILL-INDEX-START -->

[snappy-whatsapp Index]|root: ~/.claude/skills/snappy-whatsapp|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,templates.md,workflows.md}

<!-- SKILL-INDEX-END -->

Used by#

  • snappy-imessage
  • snappy-slack
  • snappy-telegram

<!-- SNAPPY-CONTRACT-VERBS-START -->

Contract verbs#

Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.

Verb Contract arguments Effect First call
mark-read message-id write npx tsx ~/.claude/skills/snappy-whatsapp/api.ts mark-read <message-id>
media to, url, caption? send npx tsx ~/.claude/skills/snappy-whatsapp/api.ts media <to> <url>
notify text send npx tsx ~/.claude/skills/snappy-whatsapp/api.ts notify "<text>"
read limit? read npx tsx ~/.claude/skills/snappy-whatsapp/api.ts read
list limit? read npx tsx ~/.claude/skills/snappy-whatsapp/api.ts list
thread from, limit? read npx tsx ~/.claude/skills/snappy-whatsapp/api.ts thread <from>
send to, message send npx tsx ~/.claude/skills/snappy-whatsapp/api.ts send <to> "<message>"

Show the result#

When an answer carries face_hint, show it with one snappy_present(<answer>) call.

See /snappy-faces for face selection. Human-facing images must crop to the

element, render at 2x on Retina, and fill the destination channel instead of

placing a small card in a full-page screenshot.

<!-- SNAPPY-CONTRACT-VERBS-END -->

---
name: snappy-whatsapp
role: WhatsApp client messaging -- check-ins, reminders, invoice nudges, onboarding
loaded-by: PreToolUse hook (auto-injected when "snappy-whatsapp" is mentioned)
---

# snappy-whatsapp -- Agent Loader

You handle Snappy's WhatsApp channel: informal client comms, check-ins, meeting reminders, invoice follow-ups, onboarding welcomes, media sends, and Robert self-notifications. Direct WhatsApp Cloud API (Meta Business API) -- no Xano middleware.

## API module

```typescript
import { sendMessage, sendMedia, notifyRobert, getRecentWhatsAppMessages } from "../snappy-whatsapp/api.ts";
```

Or CLI:
```bash
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts send "+14155551212" "Hey, quick update..."
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts media "+14155551212" "https://example.com/img.png" "Caption"
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts notify "Build completed"
```

Credentials loaded via `snappy-settings/load.ts` from `.env.cache`.

Required env vars: `WHATSAPP_TOKEN`, `WHATSAPP_PHONE_ID`, `ROBERT_PHONE`

---

## API (Meta WhatsApp Cloud API)

Base URL: `https://graph.facebook.com/v21.0/{WHATSAPP_PHONE_ID}/messages`
Auth: `Bearer $WHATSAPP_TOKEN`

All requests include `"messaging_product": "whatsapp"` in the body.

## Copy-paste patterns

```bash
# Send text
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d '{"messaging_product":"whatsapp","to":"+14155551212","type":"text","text":{"body":"Hey, quick update..."}}'

# Send image
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d '{"messaging_product":"whatsapp","to":"+14155551212","type":"image","image":{"link":"https://example.com/img.png","caption":"Dashboard screenshot"}}'

# Notify Robert (same as send text, using $ROBERT_PHONE)
curl -s -X POST "https://graph.facebook.com/v21.0/$WHATSAPP_PHONE_ID/messages" \
  -H "Content-Type: application/json" -H "Authorization: Bearer $WHATSAPP_TOKEN" \
  -d "{\"messaging_product\":\"whatsapp\",\"to\":\"$ROBERT_PHONE\",\"type\":\"text\",\"text\":{\"body\":\"Build completed\"}}"
```

## Rules

- Phone numbers MUST be E.164 (`+14155551212`) -- Meta rejects anything else
- `sendMessage(to, message)` wraps as `{ type: "text", text: { body: message } }` -- the Cloud API field is `text.body`
- `sendMedia(to, url, caption)` sends `{ type: "image", image: { link, caption } }` -- media URL must be publicly accessible HTTPS
- Always include `caption` with media sends
- One complete thought per message -- no rapid-fire multiple messages
- Mon-Fri 9am-7pm in recipient's timezone unless urgent
- Look up phone from `snappy-clients` or `snappy-knowledge` -- never hardcode

## The draft never arrives alone

`--json` on `send` is a PREVIEW and touches nothing — no send, no staged row. It
prints the message **inside the conversation it joins**:

```bash
npx tsx ~/.claude/skills/snappy-whatsapp/api.ts send "+14155551212" "Thursday 9am works." --json
# {kind, thread, threadKind, threadTotal, draft:{to, body}, doors:[Send, Later]}
```

- `thread` is the SAME rows `thread --json` prints for that correspondent. Show
  it to the person before asking them to approve anything; a draft with no
  conversation under it asks them to trust your summary of the conversation.
- `kind` says which of two situations this is: `whatsapp-decision` when a
  conversation is in hand, `whatsapp-compose` when there is none. The Cloud API
  **cannot be polled** — the only inbound is the local webhook log — so a first
  message to someone honestly has `thread: []`. Never present that as a reply.
- E.164 and the log's bare digits are matched on digits, so `+1 415 555 1212`
  finds the chat the webhook filed as `14155551212`.
- WITHOUT `--json`, `send` is unchanged: it stages for the owner's decision.

## Channel priority (when to use WhatsApp)

| Priority | Channel | Best for |
|----------|---------|----------|
| 1 | Slack | Active clients with shared channels |
| 2 | Email | Formal, async, paper trail |
| 3 | **WhatsApp** | **Quick, personal, informal** |
| 4 | iMessage | Apple-to-Apple personal contacts |
| 5 | Telegram | Robert self-notifications |

## Uses

| skill | relationship |
|-------|-------------|
| `snappy-clients` | Source of client phone, project status, comm preferences |
| `snappy-calendar` | Meeting data feeds reminder workflows |
| `snappy-freshbooks` | Overdue invoices feed nudge workflows |
| `snappy-ops` | Orchestrator -- flags stale clients, tomorrow's meetings |
| `snappy-slack` | Sibling -- primary client comms; WhatsApp adds personal touch |

---

## Full skill reference

If this loader is insufficient, load `~/.claude/skills/snappy-whatsapp/SKILL.md` as last resort. Templates: [templates.md](templates.md). Workflows: [workflows.md](workflows.md).

If this loader doesn't cover your case:
```bash
echo "[$(date -u +%FT%TZ)] snappy-whatsapp: <what was missing>" >> ~/.claude/logs/agents-md-feedback.log
```


Show produced work with `snappy-faces`: call `draw` for image channels or `lang` for MCP Apps.

<!-- SKILL-INDEX-START -->
[snappy-whatsapp Index]|root: ~/.claude/skills/snappy-whatsapp|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,templates.md,workflows.md}
<!-- SKILL-INDEX-END -->

## Used by

- `snappy-imessage`
- `snappy-slack`
- `snappy-telegram`

<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs

Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.

| Verb | Contract arguments | Effect | First call |
|---|---|---|---|
| `mark-read` | `message-id` | `write` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts mark-read <message-id>` |
| `media` | `to`, `url`, `caption?` | `send` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts media <to> <url>` |
| `notify` | `text` | `send` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts notify "<text>"` |
| `read` | `limit?` | `read` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts read` |
| `list` | `limit?` | `read` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts list` |
| `thread` | `from`, `limit?` | `read` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts thread <from>` |
| `send` | `to`, `message` | `send` | `npx tsx ~/.claude/skills/snappy-whatsapp/api.ts send <to> "<message>"` |

## Show the result

When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->

Keyboard Shortcuts

Search in document⌘K
Focus search/
Previous file tab
Next file tab
Close overlayEsc
Show shortcuts?