WhatsApp Business Cloud API — send approved template messages at scale, manage templates with approval tracking, and integrate product catalogs. Separate from wacli personal WhatsApp.
npx skills add https://github.com/davepoon/buildwithclaude --skill ops-whatsapp-biz
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.
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:-$(claude plugin config get whatsapp_business_token 2>/dev/null)}"
WABA_PHONE_ID="${WHATSAPP_PHONE_NUMBER_ID:-$(claude plugin config get whatsapp_phone_number_id 2>/dev/null)}"
WABA_ACCOUNT_ID="${WHATSAPP_BUSINESS_ACCOUNT_ID:-$(claude plugin config get whatsapp_business_account_id 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.
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 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 an approved template message to one or more recipients.
Before sending, collect via AskUserQuestion:
list-templates first to see available)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=[]
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
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
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.
Configure WhatsApp Business API credentials.
Before asking, auto-scan for existing credentials:
# 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
claude plugin config get whatsapp_business_token 2>/dev/null && echo "✓ whatsapp_business_token already configured"
claude plugin config get whatsapp_phone_number_id 2>/dev/null && echo "✓ whatsapp_phone_number_id already configured"
claude plugin config get whatsapp_business_account_id 2>/dev/null && echo "✓ whatsapp_business_account_id already configured"
If credentials not found, guide user:
[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 IDWHATSAPP_BUSINESS_ACCOUNT_ID: Meta Business Manager → Business Settings → WhatsApp Accounts → Account IDclaude plugin config set whatsapp_business_token "$WABA_TOKEN"
claude plugin config set whatsapp_phone_number_id "$WABA_PHONE_ID"
claude plugin config set whatsapp_business_account_id "$WABA_ACCOUNT_ID"
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].
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take davepoon/ops-whatsapp-biz from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.