Skip to content
Back to skills

Ops Whatsapp Biz

CSecurity

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

  • 538 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
data-aigoshellbashapidocumentation

Works with

  • cli
  • api

Security analysis

C63/100
  • criticalModifies startup scripts or system services for persistence
  • criticalModifies startup scripts or system services for persistence
  • criticalSends environment variables or credentials to an external URL

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

Scanned September 21, 2026

npx -y skills add Lifecycle-Innovations-Limited/claude-ops --skill ops-whatsapp-biz --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ops Whatsapp Biz?

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

Security grade badge for Ops Whatsapp Biz
[![Security: C — Skills Directory](https://www.skillsdirectory.com/api/skills/lifecycle-innovations-limited-ops-whatsapp-biz/badge)](https://www.skillsdirectory.com/skills/lifecycle-innovations-limited-ops-whatsapp-biz)

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: ops-whatsapp-biz
description: "OPS on-demand: This skill should be used when the user asks to \"whatsapp business\", \"template message\"…"
argument-hint: '[send-template|list-templates|create-template|check-template|catalog|setup]'
allowed-tools:
  - Bash
  - Read
  - Write
  - Grep
  - AskUserQuestion
effort: medium
maxTurns: 30
---

# 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

```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)}"
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.

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

```bash
# 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:

```json
[
  {
    "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]`

```bash
# 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.

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

```bash
# 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:

```bash
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

2. Collect each value via AskUserQuestion (free text):
   - WhatsApp Business Token
   - Phone Number ID
   - Business Account ID (WABA ID)

3. Save via plugin config:

```bash
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"
```

4. Smoke test:

```bash
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]`.

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…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.