OPS ► WHATSAPP BUSINESS skill

OPS on-demand: This skill should be used when the user asks to "whatsapp business", "template message"…

by Lifecycle-Innovations-Limited·MIT license·★ 543 Stars on the repo·GitHub ↗

Use now

Inside this skill

SKILL.md · the author's own words
# OPS ► WHATSAPP BUSINESS

Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Work…

WhatsApp Business Cloud API — distinct from wacli personal WhatsApp.

**Key difference:** This skill uses the WhatsApp Business Cloud API (Meta Graph API) for business-to-customer messaging at scale. `wacli` i…

## Credential Resolution

```bash
PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"

# WhatsApp Business credentials (separate from wacli personal)
WABA_TOKEN="${WHATSAPP_BUSINESS_TOKEN:-$(jq -r '.user_config.whatsapp_business_token // empty' "$PREFS_PATH" 2>/dev/null)}"
WABA_PHONE_ID="${WHATSAPP_PHONE_NUMBER_ID:-$(jq -r '.user_config.whatsapp_phone_number_id // empty' "$PREFS_PATH" 2>/dev/null)}"

Word-for-word excerpt, not a result. Read the full SKILL.md ↓

Files of OPS ► WHATSAPP BUSINESS

Lifecycle-Innovations-Limited/main1 file shown
SKILL.md
Show the full text398 lines

OPS ► WHATSAPP BUSINESS

Load ops-rules before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If AskUserQuestion / Workflow are missing, follow Rule 10 in ops-rules (Hermes: numbered options / two-turn Telegram card; delegate_task).

WhatsApp Business Cloud API — distinct from wacli personal WhatsApp.

Key difference: This skill uses the WhatsApp Business Cloud API (Meta Graph API) for business-to-customer messaging at scale. wacli is for personal WhatsApp messaging. Different credentials, different phone numbers, different use cases.

Credential Resolution

PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"

# WhatsApp Business credentials (separate from wacli personal)
WABA_TOKEN="${WHATSAPP_BUSINESS_TOKEN:-$(jq -r '.user_config.whatsapp_business_token // empty' "$PREFS_PATH" 2>/dev/null)}"
WABA_PHONE_ID="${WHATSAPP_PHONE_NUMBER_ID:-$(jq -r '.user_config.whatsapp_phone_number_id // empty' "$PREFS_PATH" 2>/dev/null)}"
WABA_ACCOUNT_ID="${WHATSAPP_BUSINESS_ACCOUNT_ID:-$(jq -r '.user_config.whatsapp_business_account_id // empty' "$PREFS_PATH" 2>/dev/null)}"

# Doppler fallback
if [ -z "$WABA_TOKEN" ]; then
  WABA_TOKEN="$(doppler secrets get WHATSAPP_BUSINESS_TOKEN --plain 2>/dev/null)"
fi
if [ -z "$WABA_PHONE_ID" ]; then
  WABA_PHONE_ID="$(doppler secrets get WHATSAPP_PHONE_NUMBER_ID --plain 2>/dev/null)"
fi
if [ -z "$WABA_ACCOUNT_ID" ]; then
  WABA_ACCOUNT_ID="$(doppler secrets get WHATSAPP_BUSINESS_ACCOUNT_ID --plain 2>/dev/null)"
fi

Credential check: If WABA_TOKEN or WABA_PHONE_ID is empty, print: WhatsApp Business not configured. Run /ops:whatsapp-biz setup to configure credentials. and stop.


Sub-command Routing

Route $ARGUMENTS:

Input Action
(empty), list-templates List all templates with approval status
send-template Send an approved template message to one or more recipients
create-template Guided template creation wizard
check-template <NAME> Poll approval status for a specific template
catalog View and manage linked product catalog
setup Configure WhatsApp Business API credentials

list-templates

List all message templates for the WhatsApp Business Account.

RESULT=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates?fields=name,status,category,language,components&limit=50" \
  -H "Authorization: Bearer ${WABA_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  WHATSAPP BUSINESS TEMPLATES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
printf "| %-30s | %-10s | %-12s | %-8s |\n" "Template Name" "Status" "Category" "Language"
printf "|%s|%s|%s|%s|\n" "--------------------------------" "------------" "--------------" "----------"

echo "$RESULT" | jq -r '.data[]? | [.name, .status, .category, .language] | @tsv' 2>/dev/null | \
  while IFS=$'\t' read -r name status category language; do
    STATUS_ICON="○"
    [ "$status" = "APPROVED" ] && STATUS_ICON="✓"
    [ "$status" = "REJECTED" ] && STATUS_ICON="✗"
    [ "$status" = "PENDING" ] && STATUS_ICON="…"
    printf "| %-30s | %s %-9s | %-12s | %-8s |\n" "${name:0:30}" "$STATUS_ICON" "$status" "${category:0:12}" "$language"
  done

TOTAL=$(echo "$RESULT" | jq '.data | length // 0')
APPROVED=$(echo "$RESULT" | jq '[.data[]? | select(.status == "APPROVED")] | length')
echo ""
echo "Total: ${TOTAL} templates  |  Approved: ${APPROVED}"
echo ""
echo "Note: Per-template pricing applies since July 2025. Check Meta pricing page for current rates."

send-template

Send an approved template message to one or more recipients.

Before sending, collect via AskUserQuestion:

  1. Template name (free text — use list-templates first to see available)
  2. Recipient phone number(s) — free text (single number or comma-separated list, include country code, e.g. +14155552671)

If template has variables, ask for each parameter value via AskUserQuestion (free text).

Cost note: Always display approximate cost before sending. Utility templates: ~$0.005/msg; Marketing: ~$0.025/msg (US rates — varies by country). Show AskUserQuestion to confirm before bulk sends > 10 recipients.

# Parse recipients
IFS=',' read -ra RECIPIENTS <<< "$PHONE_NUMBERS"
RECIPIENT_COUNT=${#RECIPIENTS[@]}

# For bulk sends > 10, confirm
if [ $RECIPIENT_COUNT -gt 10 ]; then
  # AskUserQuestion: "Send to ${RECIPIENT_COUNT} recipients? Estimated cost: ~$$(awk "BEGIN {printf \"%.2f\", $RECIPIENT_COUNT * 0.025}") for marketing templates." options [Send, Cancel]
  [ "$CONFIRM" = "Cancel" ] && echo "Cancelled." && exit 0
fi

# Send to each recipient
SUCCESS=0; FAILED=0
for PHONE in "${RECIPIENTS[@]}"; do
  PHONE=$(echo "$PHONE" | tr -d ' ')

  RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${WABA_PHONE_ID}/messages" \
    -H "Authorization: Bearer ${WABA_TOKEN}" \
    -H "Content-Type: application/json" \
    -d "{
      \"messaging_product\": \"whatsapp\",
      \"to\": \"${PHONE}\",
      \"type\": \"template\",
      \"template\": {
        \"name\": \"${TEMPLATE_NAME}\",
        \"language\": {\"code\": \"en_US\"},
        \"components\": ${TEMPLATE_COMPONENTS_JSON:-[]}
      }
    }")

  MSG_ID=$(echo "$RESP" | jq -r '.messages[0].id // empty')
  if [ -n "$MSG_ID" ]; then
    SUCCESS=$((SUCCESS + 1))
  else
    FAILED=$((FAILED + 1))
    ERR=$(echo "$RESP" | jq -r '.error.message // "unknown error"')
    echo "  Failed for ${PHONE}: ${ERR}"
  fi
done

echo ""
echo "Sent: ${SUCCESS}/${RECIPIENT_COUNT} messages delivered"
[ $FAILED -gt 0 ] && echo "Failed: ${FAILED} — check phone number format (+country_code + number)"

Template components format (build TEMPLATE_COMPONENTS_JSON from user input):

For templates with header + body variables:

[
  {
    "type": "header",
    "parameters": [{ "type": "text", "text": "{{header_value}}" }]
  },
  {
    "type": "body",
    "parameters": [
      { "type": "text", "text": "{{var1}}" },
      { "type": "text", "text": "{{var2}}" }
    ]
  }
]

For templates with no variables: TEMPLATE_COMPONENTS_JSON=[]


create-template

Guided template creation wizard.

Step 1 — Collect basic info:

AskUserQuestion: "Template category?" options [MARKETING, UTILITY, AUTHENTICATION, More...]

If More: AskUserQuestion: [AUTHENTICATION, UTILITY, Skip]

Step 2 — AskUserQuestion: "Template name (lowercase, underscores only, e.g. order_confirmation):" (free text)

Step 3 — AskUserQuestion: "Language?" options [en_US, en_GB, es, fr]

Step 4 — Collect body text (free text). Instruct user: "Use {{1}}, {{2}} etc for variables."

Step 5 — AskUserQuestion: "Add a header?" options [Text header, Image header, No header, Video header]

Step 6 — AskUserQuestion: "Add call-to-action button?" options [URL button, Phone button, No button, Quick reply]

# Build components array
COMPONENTS='[{"type":"BODY","text":"'"${BODY_TEXT}"'"}]'

# Add header if selected
if [ "$HEADER_TYPE" = "Text header" ]; then
  HEADER_TEXT_INPUT="..."  # collected via AskUserQuestion
  COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"HEADER","format":"TEXT","text":"'"${HEADER_TEXT_INPUT}"'"}]')
elif [ "$HEADER_TYPE" = "Image header" ]; then
  COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"HEADER","format":"IMAGE","example":{"header_handle":["<upload_handle>"]}}]')
fi

# Add button if selected
if [ "$BUTTON_TYPE" = "URL button" ]; then
  BUTTON_URL="..."  # collected via AskUserQuestion: "Button URL:"
  BUTTON_TEXT="..."  # collected via AskUserQuestion: "Button text:"
  COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"BUTTONS","buttons":[{"type":"URL","text":"'"${BUTTON_TEXT}"'","url":"'"${BUTTON_URL}"'"}]}]')
elif [ "$BUTTON_TYPE" = "Phone button" ]; then
  BUTTON_PHONE="..."  # collected via AskUserQuestion: "Phone number for button:"
  BUTTON_TEXT="..."
  COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"BUTTONS","buttons":[{"type":"PHONE_NUMBER","text":"'"${BUTTON_TEXT}"'","phone_number":"'"${BUTTON_PHONE}"'"}]}]')
fi

RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates" \
  -H "Authorization: Bearer ${WABA_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "{
    \"name\": \"${TEMPLATE_NAME}\",
    \"category\": \"${CATEGORY}\",
    \"language\": \"${LANGUAGE}\",
    \"components\": ${COMPONENTS}
  }")

TEMPLATE_ID=$(echo "$RESP" | jq -r '.id // empty')
STATUS=$(echo "$RESP" | jq -r '.status // empty')

if [ -n "$TEMPLATE_ID" ]; then
  echo "Template submitted (ID: ${TEMPLATE_ID}, status: ${STATUS})."
  echo "Approval typically takes 24-48 hours. Check status with: /ops:whatsapp-biz check-template ${TEMPLATE_NAME}"
else
  echo "Template creation failed: $(echo "$RESP" | jq -r '.error.message // "unknown error"')"
fi

check-template <NAME>

Poll approval status for a specific template by name.

TEMPLATE_NAME_ARG=$(echo "$ARGUMENTS" | awk '{print $2}')
if [ -z "$TEMPLATE_NAME_ARG" ]; then
  # AskUserQuestion: "Template name to check:" (free text)
  TEMPLATE_NAME_ARG="$USER_INPUT"
fi

RESULT=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates?name=${TEMPLATE_NAME_ARG}&fields=name,status,rejected_reason,quality_score" \
  -H "Authorization: Bearer ${WABA_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  TEMPLATE STATUS: ${TEMPLATE_NAME_ARG}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

echo "$RESULT" | jq -r '.data[]? | "Status: \(.status)\nQuality: \(.quality_score.score // "N/A")\nRejection reason: \(.rejected_reason // "—")"' 2>/dev/null

STATUS=$(echo "$RESULT" | jq -r '.data[0].status // "NOT_FOUND"')
case "$STATUS" in
  APPROVED)  echo "Template is ready to use with send-template." ;;
  REJECTED)  echo "Template was rejected. Review the rejection reason and create a revised template." ;;
  PENDING)   echo "Template is pending review (typically 24-48h)." ;;
  NOT_FOUND) echo "Template '${TEMPLATE_NAME_ARG}' not found. Run list-templates to see available templates." ;;
esac

catalog

View and manage the product catalog linked to your WhatsApp Business Account.

# List linked catalogs
CATALOGS=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}?fields=catalog_id" \
  -H "Authorization: Bearer ${WABA_TOKEN}")
CATALOG_ID=$(echo "$CATALOGS" | jq -r '.catalog_id // empty')

if [ -z "$CATALOG_ID" ]; then
  echo "No product catalog linked to this WhatsApp Business Account."
  echo "To link a catalog: go to Meta Business Manager → Commerce Manager → link your catalog to WhatsApp."
  exit 0
fi

# List products in catalog
PRODUCTS=$(curl -s "https://graph.facebook.com/v20.0/${CATALOG_ID}/products?fields=id,name,retailer_id,price,availability,url&limit=20" \
  -H "Authorization: Bearer ${WABA_TOKEN}")

echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  WHATSAPP PRODUCT CATALOG"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Catalog ID: ${CATALOG_ID}"
echo ""
printf "| %-25s | %-12s | %-10s | %-12s |\n" "Product Name" "SKU" "Price" "Availability"
printf "|%s|%s|%s|%s|\n" "---------------------------" "--------------" "------------" "--------------"
echo "$PRODUCTS" | jq -r '.data[]? | [.name, (.retailer_id // "—"), (.price // "—"), (.availability // "—")] | @tsv' 2>/dev/null | \
  while IFS=$'\t' read -r name sku price avail; do
    printf "| %-25s | %-12s | %-10s | %-12s |\n" "${name:0:25}" "${sku:0:12}" "$price" "$avail"
  done

PRODUCT_COUNT=$(echo "$PRODUCTS" | jq '.data | length // 0')
echo ""
echo "Total products shown: ${PRODUCT_COUNT} (use catalog in message templates with product cards)"

MM Lite note: Marketing Messages Lite (AI-optimized sends) is a beta API as of 2026. It is not implemented here — check Meta's developer documentation for current availability and enrollment requirements.


setup

Configure WhatsApp Business API credentials.

Before asking, auto-scan for existing credentials:

PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"
# Scan env vars
printenv WHATSAPP_BUSINESS_TOKEN WHATSAPP_PHONE_NUMBER_ID WHATSAPP_BUSINESS_ACCOUNT_ID 2>/dev/null

# Scan Doppler
for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
  for cfg in dev stg prd; do
    doppler secrets --project "$proj" --config "$cfg" --json 2>/dev/null | \
      jq -r --arg p "$proj" --arg c "$cfg" 'to_entries[] | select(.key | test("WHATSAPP_BUSINESS|WHATSAPP_PHONE"; "i")) | "\(.key)=\(.value.computed) (doppler:\($p)/\($c))"'
  done
done

# Shell profiles
grep -h 'WHATSAPP_BUSINESS\|WHATSAPP_PHONE' ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'

# Existing plugin config
jq -r '.user_config.whatsapp_business_token // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_business_token already configured"
jq -r '.user_config.whatsapp_phone_number_id // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_phone_number_id already configured"
jq -r '.user_config.whatsapp_business_account_id // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_business_account_id already configured"

If credentials not found, guide user:

  1. AskUserQuestion: "Where do you have your WhatsApp Business credentials?" options [Paste token now, Find in Meta dashboard, Skip]

Where to find credentials in Meta:

  • WHATSAPP_BUSINESS_TOKEN: Meta Developer Portal → Your App → WhatsApp → API Setup → Temporary access token (or generate a permanent System User token)
  • WHATSAPP_PHONE_NUMBER_ID: Same page → "From" phone number → Phone Number ID
  • WHATSAPP_BUSINESS_ACCOUNT_ID: Meta Business Manager → Business Settings → WhatsApp Accounts → Account ID
  1. Collect each value via AskUserQuestion (free text):

    • WhatsApp Business Token
    • Phone Number ID
    • Business Account ID (WABA ID)
  2. Save via plugin config:

PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"
ops_user_config_set() {  # write into preferences.json .user_config.<key>
  local key="$1" value="$2" tmp
  [ -n "$key" ] && [ -f "$PREFS_PATH" ] || return 0
  tmp="$(mktemp "${PREFS_PATH}.XXXXXX")" || return 0
  if jq --arg k "$key" --arg v "$value" '.user_config[$k] = $v' "$PREFS_PATH" >"$tmp" 2>/dev/null; then
    chmod 600 "$tmp"; mv -f "$tmp" "$PREFS_PATH"
  else
    rm -f "$tmp"
  fi
}

ops_user_config_set whatsapp_business_token "$WABA_TOKEN"
ops_user_config_set whatsapp_phone_number_id "$WABA_PHONE_ID"
ops_user_config_set whatsapp_business_account_id "$WABA_ACCOUNT_ID"
  1. Smoke test:
TEST=$(curl -s "https://graph.facebook.com/v20.0/${WABA_PHONE_ID}" \
  -H "Authorization: Bearer ${WABA_TOKEN}")
NAME=$(echo "$TEST" | jq -r '.display_phone_number // empty')
if [ -n "$NAME" ]; then
  echo "WhatsApp Business ✓ connected — Phone: ${NAME}"
else
  echo "WhatsApp Business ✗ connection failed: $(echo "$TEST" | jq -r '.error.message // "invalid token or phone ID"')"
fi

Report: WhatsApp Business ✓ connected or ✗ invalid — [error].

1---
2name: ops-whatsapp-biz
3description: "OPS on-demand: This skill should be used when the user asks to \"whatsapp business\", \"template message\"…"
4argument-hint: '[send-template|list-templates|create-template|check-template|catalog|setup]'
5allowed-tools:
6 - Bash
7 - Read
8 - Write
9 - Grep
10 - AskUserQuestion
11effort: medium
12maxTurns: 30
13---
14 
15# OPS ► WHATSAPP BUSINESS
16 
17Load `ops-rules` before acting. Public repo (no personal data). Outbound: one draft → one approval → one send. If `AskUserQuestion` / `Workflow` are missing, follow Rule 10 in `ops-rules` (Hermes: numbered options / two-turn Telegram card; `delegate_task`).
18 
19WhatsApp Business Cloud API — distinct from wacli personal WhatsApp.
20 
21**Key difference:** This skill uses the WhatsApp Business Cloud API (Meta Graph API) for business-to-customer messaging at scale. `wacli` is for personal WhatsApp messaging. Different credentials, different phone numbers, different use cases.
22 
23## Credential Resolution
24 
25```bash
26PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"
27 
28# WhatsApp Business credentials (separate from wacli personal)
29WABA_TOKEN="${WHATSAPP_BUSINESS_TOKEN:-$(jq -r '.user_config.whatsapp_business_token // empty' "$PREFS_PATH" 2>/dev/null)}"
30WABA_PHONE_ID="${WHATSAPP_PHONE_NUMBER_ID:-$(jq -r '.user_config.whatsapp_phone_number_id // empty' "$PREFS_PATH" 2>/dev/null)}"
31WABA_ACCOUNT_ID="${WHATSAPP_BUSINESS_ACCOUNT_ID:-$(jq -r '.user_config.whatsapp_business_account_id // empty' "$PREFS_PATH" 2>/dev/null)}"
32 
33# Doppler fallback
34if [ -z "$WABA_TOKEN" ]; then
35 WABA_TOKEN="$(doppler secrets get WHATSAPP_BUSINESS_TOKEN --plain 2>/dev/null)"
36fi
37if [ -z "$WABA_PHONE_ID" ]; then
38 WABA_PHONE_ID="$(doppler secrets get WHATSAPP_PHONE_NUMBER_ID --plain 2>/dev/null)"
39fi
40if [ -z "$WABA_ACCOUNT_ID" ]; then
41 WABA_ACCOUNT_ID="$(doppler secrets get WHATSAPP_BUSINESS_ACCOUNT_ID --plain 2>/dev/null)"
42fi
43```
44 
45**Credential check**: If `WABA_TOKEN` or `WABA_PHONE_ID` is empty, print:
46`WhatsApp Business not configured. Run /ops:whatsapp-biz setup to configure credentials.`
47and stop.
48 
49---
50 
51## Sub-command Routing
52 
53Route `$ARGUMENTS`:
54 
55| Input | Action |
56| ----------------------- | ----------------------------------------------------------- |
57| (empty), list-templates | List all templates with approval status |
58| send-template | Send an approved template message to one or more recipients |
59| create-template | Guided template creation wizard |
60| check-template \<NAME\> | Poll approval status for a specific template |
61| catalog | View and manage linked product catalog |
62| setup | Configure WhatsApp Business API credentials |
63 
64---
65 
66## list-templates
67 
68List all message templates for the WhatsApp Business Account.
69 
70```bash
71RESULT=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates?fields=name,status,category,language,components&limit=50" \
72 -H "Authorization: Bearer ${WABA_TOKEN}")
73 
74echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
75echo " WHATSAPP BUSINESS TEMPLATES"
76echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
77echo ""
78printf "| %-30s | %-10s | %-12s | %-8s |\n" "Template Name" "Status" "Category" "Language"
79printf "|%s|%s|%s|%s|\n" "--------------------------------" "------------" "--------------" "----------"
80 
81echo "$RESULT" | jq -r '.data[]? | [.name, .status, .category, .language] | @tsv' 2>/dev/null | \
82 while IFS=$'\t' read -r name status category language; do
83 STATUS_ICON="○"
84 [ "$status" = "APPROVED" ] && STATUS_ICON="✓"
85 [ "$status" = "REJECTED" ] && STATUS_ICON="✗"
86 [ "$status" = "PENDING" ] && STATUS_ICON="…"
87 printf "| %-30s | %s %-9s | %-12s | %-8s |\n" "${name:0:30}" "$STATUS_ICON" "$status" "${category:0:12}" "$language"
88 done
89 
90TOTAL=$(echo "$RESULT" | jq '.data | length // 0')
91APPROVED=$(echo "$RESULT" | jq '[.data[]? | select(.status == "APPROVED")] | length')
92echo ""
93echo "Total: ${TOTAL} templates | Approved: ${APPROVED}"
94echo ""
95echo "Note: Per-template pricing applies since July 2025. Check Meta pricing page for current rates."
96```
97 
98---
99 
100## send-template
101 
102Send an approved template message to one or more recipients.
103 
104**Before sending**, collect via AskUserQuestion:
105 
1061. Template name (free text — use `list-templates` first to see available)
1072. Recipient phone number(s) — free text (single number or comma-separated list, include country code, e.g. +14155552671)
108 
109If template has variables, ask for each parameter value via AskUserQuestion (free text).
110 
111**Cost note**: Always display approximate cost before sending. Utility templates: ~$0.005/msg; Marketing: ~$0.025/msg (US rates — varies by country). Show AskUserQuestion to confirm before bulk sends > 10 recipients.
112 
113```bash
114# Parse recipients
115IFS=',' read -ra RECIPIENTS <<< "$PHONE_NUMBERS"
116RECIPIENT_COUNT=${#RECIPIENTS[@]}
117 
118# For bulk sends > 10, confirm
119if [ $RECIPIENT_COUNT -gt 10 ]; then
120 # AskUserQuestion: "Send to ${RECIPIENT_COUNT} recipients? Estimated cost: ~$$(awk "BEGIN {printf \"%.2f\", $RECIPIENT_COUNT * 0.025}") for marketing templates." options [Send, Cancel]
121 [ "$CONFIRM" = "Cancel" ] && echo "Cancelled." && exit 0
122fi
123 
124# Send to each recipient
125SUCCESS=0; FAILED=0
126for PHONE in "${RECIPIENTS[@]}"; do
127 PHONE=$(echo "$PHONE" | tr -d ' ')
128 
129 RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${WABA_PHONE_ID}/messages" \
130 -H "Authorization: Bearer ${WABA_TOKEN}" \
131 -H "Content-Type: application/json" \
132 -d "{
133 \"messaging_product\": \"whatsapp\",
134 \"to\": \"${PHONE}\",
135 \"type\": \"template\",
136 \"template\": {
137 \"name\": \"${TEMPLATE_NAME}\",
138 \"language\": {\"code\": \"en_US\"},
139 \"components\": ${TEMPLATE_COMPONENTS_JSON:-[]}
140 }
141 }")
142 
143 MSG_ID=$(echo "$RESP" | jq -r '.messages[0].id // empty')
144 if [ -n "$MSG_ID" ]; then
145 SUCCESS=$((SUCCESS + 1))
146 else
147 FAILED=$((FAILED + 1))
148 ERR=$(echo "$RESP" | jq -r '.error.message // "unknown error"')
149 echo " Failed for ${PHONE}: ${ERR}"
150 fi
151done
152 
153echo ""
154echo "Sent: ${SUCCESS}/${RECIPIENT_COUNT} messages delivered"
155[ $FAILED -gt 0 ] && echo "Failed: ${FAILED} — check phone number format (+country_code + number)"
156```
157 
158**Template components format** (build `TEMPLATE_COMPONENTS_JSON` from user input):
159 
160For templates with header + body variables:
161 
162```json
163[
164 {
165 "type": "header",
166 "parameters": [{ "type": "text", "text": "{{header_value}}" }]
167 },
168 {
169 "type": "body",
170 "parameters": [
171 { "type": "text", "text": "{{var1}}" },
172 { "type": "text", "text": "{{var2}}" }
173 ]
174 }
175]
176```
177 
178For templates with no variables: `TEMPLATE_COMPONENTS_JSON=[]`
179 
180---
181 
182## create-template
183 
184Guided template creation wizard.
185 
186Step 1 — Collect basic info:
187 
188AskUserQuestion: "Template category?" options `[MARKETING, UTILITY, AUTHENTICATION, More...]`
189 
190If More: AskUserQuestion: `[AUTHENTICATION, UTILITY, Skip]`
191 
192Step 2 — AskUserQuestion: "Template name (lowercase, underscores only, e.g. order_confirmation):" (free text)
193 
194Step 3 — AskUserQuestion: "Language?" options `[en_US, en_GB, es, fr]`
195 
196Step 4 — Collect body text (free text). Instruct user: "Use {{1}}, {{2}} etc for variables."
197 
198Step 5 — AskUserQuestion: "Add a header?" options `[Text header, Image header, No header, Video header]`
199 
200Step 6 — AskUserQuestion: "Add call-to-action button?" options `[URL button, Phone button, No button, Quick reply]`
201 
202```bash
203# Build components array
204COMPONENTS='[{"type":"BODY","text":"'"${BODY_TEXT}"'"}]'
205 
206# Add header if selected
207if [ "$HEADER_TYPE" = "Text header" ]; then
208 HEADER_TEXT_INPUT="..." # collected via AskUserQuestion
209 COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"HEADER","format":"TEXT","text":"'"${HEADER_TEXT_INPUT}"'"}]')
210elif [ "$HEADER_TYPE" = "Image header" ]; then
211 COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"HEADER","format":"IMAGE","example":{"header_handle":["<upload_handle>"]}}]')
212fi
213 
214# Add button if selected
215if [ "$BUTTON_TYPE" = "URL button" ]; then
216 BUTTON_URL="..." # collected via AskUserQuestion: "Button URL:"
217 BUTTON_TEXT="..." # collected via AskUserQuestion: "Button text:"
218 COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"BUTTONS","buttons":[{"type":"URL","text":"'"${BUTTON_TEXT}"'","url":"'"${BUTTON_URL}"'"}]}]')
219elif [ "$BUTTON_TYPE" = "Phone button" ]; then
220 BUTTON_PHONE="..." # collected via AskUserQuestion: "Phone number for button:"
221 BUTTON_TEXT="..."
222 COMPONENTS=$(echo "$COMPONENTS" | jq '. + [{"type":"BUTTONS","buttons":[{"type":"PHONE_NUMBER","text":"'"${BUTTON_TEXT}"'","phone_number":"'"${BUTTON_PHONE}"'"}]}]')
223fi
224 
225RESP=$(curl -s -X POST "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates" \
226 -H "Authorization: Bearer ${WABA_TOKEN}" \
227 -H "Content-Type: application/json" \
228 -d "{
229 \"name\": \"${TEMPLATE_NAME}\",
230 \"category\": \"${CATEGORY}\",
231 \"language\": \"${LANGUAGE}\",
232 \"components\": ${COMPONENTS}
233 }")
234 
235TEMPLATE_ID=$(echo "$RESP" | jq -r '.id // empty')
236STATUS=$(echo "$RESP" | jq -r '.status // empty')
237 
238if [ -n "$TEMPLATE_ID" ]; then
239 echo "Template submitted (ID: ${TEMPLATE_ID}, status: ${STATUS})."
240 echo "Approval typically takes 24-48 hours. Check status with: /ops:whatsapp-biz check-template ${TEMPLATE_NAME}"
241else
242 echo "Template creation failed: $(echo "$RESP" | jq -r '.error.message // "unknown error"')"
243fi
244```
245 
246---
247 
248## check-template \<NAME\>
249 
250Poll approval status for a specific template by name.
251 
252```bash
253TEMPLATE_NAME_ARG=$(echo "$ARGUMENTS" | awk '{print $2}')
254if [ -z "$TEMPLATE_NAME_ARG" ]; then
255 # AskUserQuestion: "Template name to check:" (free text)
256 TEMPLATE_NAME_ARG="$USER_INPUT"
257fi
258 
259RESULT=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}/message_templates?name=${TEMPLATE_NAME_ARG}&fields=name,status,rejected_reason,quality_score" \
260 -H "Authorization: Bearer ${WABA_TOKEN}")
261 
262echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
263echo " TEMPLATE STATUS: ${TEMPLATE_NAME_ARG}"
264echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
265echo ""
266 
267echo "$RESULT" | jq -r '.data[]? | "Status: \(.status)\nQuality: \(.quality_score.score // "N/A")\nRejection reason: \(.rejected_reason // "—")"' 2>/dev/null
268 
269STATUS=$(echo "$RESULT" | jq -r '.data[0].status // "NOT_FOUND"')
270case "$STATUS" in
271 APPROVED) echo "Template is ready to use with send-template." ;;
272 REJECTED) echo "Template was rejected. Review the rejection reason and create a revised template." ;;
273 PENDING) echo "Template is pending review (typically 24-48h)." ;;
274 NOT_FOUND) echo "Template '${TEMPLATE_NAME_ARG}' not found. Run list-templates to see available templates." ;;
275esac
276```
277 
278---
279 
280## catalog
281 
282View and manage the product catalog linked to your WhatsApp Business Account.
283 
284```bash
285# List linked catalogs
286CATALOGS=$(curl -s "https://graph.facebook.com/v20.0/${WABA_ACCOUNT_ID}?fields=catalog_id" \
287 -H "Authorization: Bearer ${WABA_TOKEN}")
288CATALOG_ID=$(echo "$CATALOGS" | jq -r '.catalog_id // empty')
289 
290if [ -z "$CATALOG_ID" ]; then
291 echo "No product catalog linked to this WhatsApp Business Account."
292 echo "To link a catalog: go to Meta Business Manager → Commerce Manager → link your catalog to WhatsApp."
293 exit 0
294fi
295 
296# List products in catalog
297PRODUCTS=$(curl -s "https://graph.facebook.com/v20.0/${CATALOG_ID}/products?fields=id,name,retailer_id,price,availability,url&limit=20" \
298 -H "Authorization: Bearer ${WABA_TOKEN}")
299 
300echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
301echo " WHATSAPP PRODUCT CATALOG"
302echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
303echo "Catalog ID: ${CATALOG_ID}"
304echo ""
305printf "| %-25s | %-12s | %-10s | %-12s |\n" "Product Name" "SKU" "Price" "Availability"
306printf "|%s|%s|%s|%s|\n" "---------------------------" "--------------" "------------" "--------------"
307echo "$PRODUCTS" | jq -r '.data[]? | [.name, (.retailer_id // "—"), (.price // "—"), (.availability // "—")] | @tsv' 2>/dev/null | \
308 while IFS=$'\t' read -r name sku price avail; do
309 printf "| %-25s | %-12s | %-10s | %-12s |\n" "${name:0:25}" "${sku:0:12}" "$price" "$avail"
310 done
311 
312PRODUCT_COUNT=$(echo "$PRODUCTS" | jq '.data | length // 0')
313echo ""
314echo "Total products shown: ${PRODUCT_COUNT} (use catalog in message templates with product cards)"
315```
316 
317**MM Lite note**: Marketing Messages Lite (AI-optimized sends) is a beta API as of 2026. It is not implemented here — check Meta's developer documentation for current availability and enrollment requirements.
318 
319---
320 
321## setup
322 
323Configure WhatsApp Business API credentials.
324 
325**Before asking**, auto-scan for existing credentials:
326 
327```bash
328PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"
329# Scan env vars
330printenv WHATSAPP_BUSINESS_TOKEN WHATSAPP_PHONE_NUMBER_ID WHATSAPP_BUSINESS_ACCOUNT_ID 2>/dev/null
331 
332# Scan Doppler
333for proj in $(doppler projects --json 2>/dev/null | jq -r '.[].slug'); do
334 for cfg in dev stg prd; do
335 doppler secrets --project "$proj" --config "$cfg" --json 2>/dev/null | \
336 jq -r --arg p "$proj" --arg c "$cfg" 'to_entries[] | select(.key | test("WHATSAPP_BUSINESS|WHATSAPP_PHONE"; "i")) | "\(.key)=\(.value.computed) (doppler:\($p)/\($c))"'
337 done
338done
339 
340# Shell profiles
341grep -h 'WHATSAPP_BUSINESS\|WHATSAPP_PHONE' ~/.zshrc ~/.bashrc ~/.zprofile ~/.envrc 2>/dev/null | grep -v '^#'
342 
343# Existing plugin config
344jq -r '.user_config.whatsapp_business_token // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_business_token already configured"
345jq -r '.user_config.whatsapp_phone_number_id // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_phone_number_id already configured"
346jq -r '.user_config.whatsapp_business_account_id // empty' "$PREFS_PATH" 2>/dev/null && echo "✓ whatsapp_business_account_id already configured"
347```
348 
349**If credentials not found**, guide user:
350 
3511. AskUserQuestion: "Where do you have your WhatsApp Business credentials?" options `[Paste token now, Find in Meta dashboard, Skip]`
352 
353**Where to find credentials in Meta:**
354 
355- `WHATSAPP_BUSINESS_TOKEN`: Meta Developer Portal → Your App → WhatsApp → API Setup → Temporary access token (or generate a permanent System User token)
356- `WHATSAPP_PHONE_NUMBER_ID`: Same page → "From" phone number → Phone Number ID
357- `WHATSAPP_BUSINESS_ACCOUNT_ID`: Meta Business Manager → Business Settings → WhatsApp Accounts → Account ID
358 
3592. Collect each value via AskUserQuestion (free text):
360 - WhatsApp Business Token
361 - Phone Number ID
362 - Business Account ID (WABA ID)
363 
3643. Save via plugin config:
365 
366```bash
367PREFS_PATH="${CLAUDE_PLUGIN_DATA_DIR:-$HOME/.claude/plugins/data/ops-ops-marketplace}/preferences.json"
368ops_user_config_set() { # write into preferences.json .user_config.<key>
369 local key="$1" value="$2" tmp
370 [ -n "$key" ] && [ -f "$PREFS_PATH" ] || return 0
371 tmp="$(mktemp "${PREFS_PATH}.XXXXXX")" || return 0
372 if jq --arg k "$key" --arg v "$value" '.user_config[$k] = $v' "$PREFS_PATH" >"$tmp" 2>/dev/null; then
373 chmod 600 "$tmp"; mv -f "$tmp" "$PREFS_PATH"
374 else
375 rm -f "$tmp"
376 fi
377}
378 
379ops_user_config_set whatsapp_business_token "$WABA_TOKEN"
380ops_user_config_set whatsapp_phone_number_id "$WABA_PHONE_ID"
381ops_user_config_set whatsapp_business_account_id "$WABA_ACCOUNT_ID"
382```
383 
3844. Smoke test:
385 
386```bash
387TEST=$(curl -s "https://graph.facebook.com/v20.0/${WABA_PHONE_ID}" \
388 -H "Authorization: Bearer ${WABA_TOKEN}")
389NAME=$(echo "$TEST" | jq -r '.display_phone_number // empty')
390if [ -n "$NAME" ]; then
391 echo "WhatsApp Business ✓ connected — Phone: ${NAME}"
392else
393 echo "WhatsApp Business ✗ connection failed: $(echo "$TEST" | jq -r '.error.message // "invalid token or phone ID"')"
394fi
395```
396 
397Report: `WhatsApp Business ✓ connected` or `✗ invalid — [error]`.
398 

Discussion