Coding

WhatsApp Business Ops

Try it

Run WhatsApp Business outbound without losing quality score — 24h window, approved templates, duplicate guard. Use when answering inbound leads or sending fo...

What it does

**WhatsApp is not a discovery channel — it is a conversion channel.** The goal is not to send fast: it is one qualified hand-off per opt-in lead, no unsolicited bulk, no duplicate send. Reference BSP: Whatchimp (Meta Business Partner); any BSP works.

The skill document

WhatsApp Business Management

WhatsApp is not a discovery channel — it is a conversion channel. The goal is not to send fast: it is one qualified hand-off per opt-in lead, no unsolicited bulk, no duplicate send. Reference BSP: Whatchimp (Meta Business Partner); any BSP works.

Access, data, and network — read before running

Every access is opt-in — each one stays off until you fill its placeholder in ## Configure. The default is no network.

AccessWhyDefault
BSP API token (``)Send/read messagesNone — no network call without it
CRM sheet ID (``)One row per leadNone — CRM writes skipped
Alert channel / webhook (``)Leaves your machine. Posts lead name + phone to Telegram/Slack/DiscordNone — recap stays local
Local workspace (/memory/)State + duplicate guardLocal files only

Personal data persisted, in full: display name, case type, free-text situation, conversation timestamps, template-send log, alert log, per-run recaps. Phone numbers are stored raw ONLY in wa-crm-state.md (the system-of-record mirror); every duplicate register and the blacklist key on sha256(phone)[0:12]. That is lead PII on disk — ## Data minimization and retention is doctrine, not an appendix. Scope the API token to the one business number; never reuse a full-account token.

When to Use

TriggerAction
"answer the whatsapp leads"wa-inbound run — §Flow, free-form only inside open window
"send the first message to new leads"wa-outbound run — approved template only
"relance the cold leads"wa-followup run — cadence table
"quality score dropped" / "number got paused"Stop outbound. ## Troubleshooting, last two rows. Human review
"am I talking to a bot?" (asked by a lead)Answer honestly, immediately — ## Identity

Configure

PlaceholderExampleYour value
``"Acme Studio"
``"+1 555 0000"
``numeric ID from your BSP
``scoped to one number
`` / _FOLLOWUP_SOFT / _FOLLOWUP_HARD / _CLOSINGMeta-approved template IDs
``Sheet/Airtable ID— (omit → no CRM write)
``alert chat ID— (omit → local recap only)
``"~/.openclaw/workspace/whatsapp-acme"
# /config.yaml
whatsapp:
  phone_number_id: 
  base_url: https://app.whatchimp.com/api/v1   # swap host for another BSP
templates:
  first_contact: 
  followup_soft: 
  followup_hard: 
  closing: 
crm: { sheet_id: , tab: "Leads" }
alerts: { primary_chat: , channel: telegram }
retention: { lead_days: 90, recap_days: 30, template_log_days: 30 }
inbound_pipeline:
  shared_queue_file: /../shared/leads-whatsapp.json
schedule:  # dm_check "*/2 9-22 * * *" · add_contacts "*/5 9-22 * * *" · follow_up "0 10 * * *"
  timezone: Europe/Paris

The 24h window — the central law

After any message a user sends you, you have 24 h to reply in free-form. After 24 h of user silence, only Meta-approved templates, each billed as a new conversation. Free-form to a cold conversation → API rejection or a marketing-conversation bill + a quality hit; exit code 2, always skipped. Check window state before every outbound send:

curl -s "https://app.whatchimp.com/api/v1/whatsapp/get/conversation" \
  -d "apiToken=" -d "phone_number_id=" \
  -d "phone_number=" -d "limit=1"
# Output: {"status":1,"data":[{"sender":"user","created_at":"2026-07-16T09:12:04Z"}]}
# sender="user" and < 24h old  → free text OK. Otherwise → template.

Session check first on every cron:

curl -s "https://app.whatchimp.com/api/v1/whatsapp/subscriber/list" \
  -d "apiToken=" -d "phone_number_id=" \
  -d "limit=1" -d "offset=1"
# Output: {"status":1,"data":[...]}   401/403 → stop+alert · 429 → stop, wait next run

Timeout > 10 s → BSP down: one retry, then stop and alert. Never retry in a loop. A 200 OK carrying status=0 is a failure, not a send. Never send a "test" message to a real lead — use a dedicated test number. Never auto-rotate an API token from inside a cron; rotation is a human action.

Phase gating and quotas

Meta assigns each number a tier: T1 1k uniques/24h · T2 10k · T3 100k · T4 unlimited. New numbers start at T1. Tiers rise on volume + quality, and fall — or the number gets paused — when quality drops.

ConditionPhaseOutbound
Tier 1 or quality yellow/redAInbound replies + approved-template follow-ups. ≤ 50 first-contact/day
Tier 2+ and quality green, no drop in 14 dBFull doctrine, first week capped at 50% of Phase B max

Read memory/wa-state.md at start of every run. Phase B is a manual flip, never automatic. Any quality drop → revert to A immediately.

Action (hard limits)Phase APhase B
Inbound replies / cron run2050
First-contact templates / 24 h50500
Follow-up templates / 24 h20200
Unique recipients / 24 h800 (under T1's 1000 cap)tier cap − 10%
Conversations / cron run3080
Same user — outbound spacingmin 24 h between any two outbound

Follow-up cadence: J+1 soft · J+3 soft · J+7 hard · J+15 hard · J+20 closing → status=lost. Stop after the closing template. Continuing past J+20 is ban-risk territory.

Duplicate guard — the most important section

Quality-score collapse is dominated by duplicate template sends. Three registers, read before every outbound action:

RegisterRow formatRule
wa-alerts-sent.md{"phone_hash":"","qualified_date":"YYYY-MM-DD"}Phone present → never alert twice
wa-template-log.md{"phone_hash":"","template_id":"","sent_at":"","result":"ok|error"}Same (phone, template) in last 24 h → SKIP
wa-crm-state.mdone row per phone, never twoOnly wa-outbound appends; every other cron UPDATEs

If in doubt, SKIP. It is always better to skip than to send a duplicate. This one rule is the difference between a number that stays Tier 2 for years and one paused in 30 days.

Alert only when a lead replies AND qualifies — never after a first template send. Alerting on first contact generates dozens of false alerts and trains the human team to ignore the channel.

Data minimization and retention

The registers above hold lead PII — a regulated store, not logs.

RuleConcretely
MinimizeStore phone + name + case type + status. Never store ID numbers, health details, case documents, or payment data in memory files — keep those in the system of record.
MaskKey the duplicate registers on sha256(phone)[0:12], not the raw number. Lookups work identically; a leaked log exposes nothing.
Restrictchmod 700 /memory. One number = one workspace. Never share wa-template-log.md across numbers or brands.
RetainLeads 90 d after last contact, recaps 30 d, template log 30 d — then delete. Prune at the start of the daily run, not "someday": find "/memory" -name 'wa-recaps.md' -mtime +30 -delete (silent — pruned, not archived).
Alert payloadName + case type + phone only. Never paste conversation transcripts into the alert channel — it leaves your machine and is usually a wider audience than the case file.
EraseOn an opt-out or deletion request: purge every register, add the hash to wa-blacklist.md, confirm to the lead. Blacklist stores hashes only.
Sensitive nichesLegal, medical, financial: confirm with your DPO/counsel before any cron writes lead data. This skill does not make that call for you.

Identity — disclose, never dodge

If a lead asks whether they are talking to a bot, a human, or an AI: say so plainly, in the same message, and offer the human.

You're talking to 's automated assistant. I can take your details
now and a specialist will call you back — or I can pass you to them right away.

No ambiguity, no pivot, no "I'm part of the team". Honest disclosure is not a cost here: a lead who discovers the automation later is a complaint, a block, and a quality-score hit. Blocks and reports are exactly what Meta measures. Disclosure is account safety. Escalate to a human — stop the automated replies for that thread and alert — when the lead asks for a human, asks twice about identity, or raises anything the templates do not cover.

Reply rules (free-form, inside open window)

  • Match tone. Short messages. One ask per message. Reassuring on stressful topics, never dramatizing. Reread the full visible history before composing.
  • Never: definitive price quotes (soft "starting at" only if asked) · outcome guarantees · confidential client info · documents/PDFs to non-clients · anything outside the brand's scope.

Skeletons: A greeting + "how can we help" · B empathy + one contextual question · C hand-off: Thank you for these details. A specialist from will call you back. Could you confirm your name and the best time to reach you?

Classify every thread before drafting — case type is what the alert payload and the recap carry. Structure below, questions are yours:

Case typeSignalAction
A — high urgencyPending deadline, acute problemAsk date, location, urgency → collect contact → qualified
B — mid intentOngoing situation, no deadline5-7 contextual questions → collect contact → qualified
C — out of scopeNot what the brand doesRedirect to the right channel. Never force qualification
D — existing clientPhone-match in wa-clients-known.mdAnswer with empathy, redirect to the standard client support line. Never paste the prospect CTA — a paying client who gets a sales pitch blocks, and a block is a quality hit

"Too many questions" failsafe — after 5 questions with no contact info, switch: "To answer precisely, a specialist needs to call you back. Could you confirm your name + best time?" This converts more question-only threads than any other tactic.

Qualification requires: thread open · not blacklisted · not already "qualified — awaiting callback" · no auto-reply in the last 60 s (anti-cascade). Any check fails → skip.

Flow

wa-inbound (every 2 min)

1. Session check. 2. subscriber/list unseen_count>0 — if 0 unread → STOP.
3. Per unread (≤20 A / ≤50 B): get/conversation limit=20 → if last sender="bot" SKIP
   → qualify → draft → normalize encoding → POST /whatsapp/send → verify status=1.
4. If qualified: check wa-alerts-sent.md → if absent, alert primary chat, append hash.
   UPDATE the CRM row (wa-inbound never APPENDs). 5. Recap.

wa-outbound (every 5 min)

1. Session check. 2. Read shared queue, filter status="pending" — if 0 → STOP.
3. Per lead: template-log 24h check → subscriber/create → POST /whatsapp/send/template
   template_id= → verify status=1 → queue status="first_message_sent"
   → APPEND CRM row (only cron that appends) → append template log.
4. Never alert from this cron. 5. Recap.

wa-followup (daily 10:00)

1. Session check. 2. CRM: status in {first_message_sent, follow_up_sent}, idle at a J+N mark.
3. Per candidate: template-log check → send cadence template → update CRM + log. 4. Recap.

Running Playwright against web.whatsapp.com is outside WhatsApp's Business Terms and risks a permanent number ban. It is documented here only as a read-only contingency while BSP approval is pending, and it is not recommended for production. Requires an explicit human decision per incident — an agent must not choose this path on its own. Sessions rotate the QR link unpredictably (daily manual re-login); no template sends are possible; quality score still applies at the number level. If a login challenge or automation check appears: stop and hand the session to a human. Never tune timing to avoid a detection check. Migrate back to the BSP API as soon as approval lands.

Exit codes

CodeMeaningRecap
0Sent, or nothing to do (zero unread)status: ok
1Fatal (API 5xx, JSON parse fail)status: error, alert
224h-window violation attempted → skippedstatus: skip, log
3Auth fail (401/403)status: blocked, alert now
4Duplicate-guard SKIPstatus: ok

Output Format — mandatory recap

Every run ends with this, verbatim, to the alert channel and appended to memory/wa-recaps.md:

[Job name] — [status: ok|partial|blocked|skipped]
Inbound replies: [N or "—"]
Templates sent: [N or "—"]
Qualified leads: [count + first names only]
Phase / Tier: [A|B / Tier 1|2|3|4]
Quality score: [green|yellow|red]
Blockers: [text OR "—"]
Next action: [1 line]

Filled instance:

wa-inbound 10:32 — status: ok
Inbound replies: 7
Templates sent: —
Qualified leads: 2 (Marc, Lina)
Phase / Tier: A / Tier 1
Quality score: green
Blockers: —
Next action: human callbacks due before 12:00

Better silence than spam. Better a blockage report than a fake success. Never fake a successful send.

Troubleshooting

SymptomRoot causeFix
200 OK, message never deliveredphoneNumberID vs phone_number_id — BSP casing differs per endpoint. No error is raised, so you'll believe your code is rightCheck casing per endpoint. Verify status=1 in the body, not just HTTP 200
Send rejected for unknown reasonLeading + in the number. 33612345678 works, +33612345678 often does notStrip + before every call
Accents vanish in delivered messagesSome BSPs munge non-ASCII silentlyRoundtrip test once (send accented → fetch back). If it munges: strip accents, emojis, typographic quotes, em-dashes before send
Message body shows an empty slotTemplate variable {{1}} sent with no parameter — silently renders blankPass every slot explicitly
Templates sent from the wrong numberMultiple bots on one BSP account, active bot not verifiedVerify active bot before the first send of every cron
"outside 24h window" errorFree-form on a closed conversationSwitch to template, retry once
Inbound message seen after 24 hWebhook latency — the window closed while nothing errored, so no alarm firedReopen with a template: UTILITY category if one exists, MARKETING otherwise (different billing and Meta tolerance). Explain the delay in a variable slot. Log in wa-incidents.md, audit webhook latency
"template not approved"Meta rejected or paused itStop using it. Pick the fallback template. Never re-approve by editing content post-approval
Quality score yellow → red overnightScore lags hours, not minutes — one bad run degrades it for 1-2 daysYellow: flip to Phase A, audit template content. Red: inbound only, alert, manual review of 7 days
Number paused by MetaUsually: outbound to non-opted-in leads, duplicate templates, or content driftStop everything. Review last 200 actions. Appeal via BSP. Never spin up a second number to get around it — Meta cross-checks businesses

Memory files

memory/: wa-state.md (phase/tier/quality) · wa-alerts-sent.md · wa-template-log.md · wa-crm-state.md · wa-blacklist.md (hashes) · wa-clients-known.md · wa-recaps.md · wa-learnings.md · wa-incidents.md. Shared: ../shared/leads-whatsapp.json (written by upstream social agents, read by wa-outbound).

mkdir -p "/memory" && chmod 700 "$_" && cd "$_" && for f in wa-recaps wa-state \
  wa-alerts-sent wa-template-log wa-crm-state wa-blacklist wa-clients-known wa-learnings \
  wa-incidents; do [ -f "$f.md" ] || : > "$f.md"; done
mkdir -p "/../shared" && Q="/../shared/leads-whatsapp.json"
[ -f "$Q" ] || echo '[]' > "$Q"   # wa-outbound reads this at step 2 — absent = first run crash
# Output: (silent, idempotent) — existing files are never truncated

First-run checklist

  • Placeholders filled; token scoped to one number.
  • BSP account approved, business number verified by Meta, ≥3 templates approved.
  • memory/ exists, chmod 700, retention values set in config.yaml.
  • Alert channel tested with "hello" — and confirmed as an acceptable destination for lead names.
  • Opt-in pipeline documented: how each lead consented, and where that proof lives.
  • Phase A confirmed, quality green.
  • Human team review: who reads alerts, callback SLA, escalation path, who handles deletion requests.

Scope

This skill ONLY: operates a WhatsApp Business number through a BSP API you own · sends free-form inside an open 24h window and Meta-approved templates outside it · contacts leads who opted in on another channel · qualifies and hands off to a human · keeps a masked, retention-bounded local state to avoid duplicates.

This skill NEVER: sends unsolicited bulk or non-opted-in outbound · sends free-form to a cold conversation · hides or denies the automation when a lead asks · adapts timing or behavior to slip past an automated check — when one appears, it stops and a human takes over · stores case documents or special-category data in memory files · retains lead data past the configured window · creates a second number to work around a Meta pause · rotates tokens or restarts infrastructure by itself.

Meta's WhatsApp Business Terms, the WhatsApp Business Messaging Policy, and your jurisdiction's privacy law are binding constraints on every run in this document. Where this doctrine and those rules disagree, those rules win.

Related skills

Post videos, photos, text, and documents to 10 social platforms through a single REST API call.

by victorcavero14375 installs50 stars

Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.

by Iván854 installs69 stars

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai21 installs8 stars

Query Twitter/X profiles, tweets, follower events, and KOL data through the 6551 REST API.

by infra403840 installs27 stars

Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.

by fly0pants4.3k installs296 stars

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván555 installs18 stars

More from alexbloch-ia

Browse all skills

Operate a Reddit account from an agent or cron without tripping automod, shadowbans, or sub bans.

by alexbloch-ia19 installs1 stars

Operating doctrine for scheduled X/Twitter account automation that protects account reputation over posting volume.

by alexbloch-ia15 installs1 stars

Reply to Instagram DMs and comments through Meta Business Suite, staying inside quotas to avoid action blocks.

by alexbloch-ia14 installs1 stars

Run a TikTok Business account from cron — inbound DM/comment replies, quotas, phase gates, stop-on-block. Use when scheduling TikTok replies. Trigger on "tik...

by alexbloch-ia14 installs

Operating doctrine for Facebook Page automation — careful brand-page pattern via Meta Business Suite, role separation, page-vs-profile-vs-group surface aware...

by alexbloch-ia17 installs1 stars

Replicate one agent across N clients without drift — one context source, a generator that refuses unknowns. Use when duplicating an agent deployment. Trigger...

by alexbloch-ia2 installs