R6 — WhatsApp Anti-Ban Policy Compliance & Rate-Limit Store Choice
Status: Research / decision input
Date: 2026-10-08
Scope: openwa-oci-infra (OpenWA gateway on OCI Ampere A1, arm64, 12 GB RAM) + planned BFF (docs/04-e2e-encryption.md §4.4 "No bulk send", default 60 msg/min per tenant)
Author: research subagent
Method: primary-source reading of Meta/WhatsApp policy pages (fetched 2026-10-08) + GitHub issue corpora from the two engine libraries OpenWA wraps (whatsapp-web.js, Baileys) + provider pricing pages. Community claims are labelled; official policy is quoted verbatim with URLs.
Reading rule for this document. Anything under OFFICIAL is a direct quote or faithful paraphrase of a Meta/WhatsApp page that I fetched and read. Anything under COMMUNITY is a maintainer/user report from a GitHub issue or a vendor blog; it is evidence of observed behaviour, not of policy. Anything marked UNVERIFIED is a claim I could not confirm against a primary source and should not be relied on for a go/no-go decision.
0. Executive summary (the honest version)
- The product as designed is, in the strictest reading, already out of compliance — not because of the 60 msg/min number, but because it is a multi-tenant gateway that resells WhatsApp access over a network. Two separate WhatsApp legal documents prohibit exactly this. This is the biggest finding in this report and it is independent of rate limits, feature set, or engine choice. See §6.
- 60 msg/min per tenant is reckless as a default and safe only as a ceiling for warmed, established, inbound-replying accounts. The defensible default for a session-based gateway is far lower, and the correct shape is not a flat rate at all — it is a warm-up schedule + per-chat pacing + cold-reachout cap + failure breaker. OpenWA already has the right primitives (
SEND_PACING_ENABLED,SEND_PACING_WARMUP_SCHEDULE,SEND_PACING_COLD_DAILY_CAP) but ships them off by default. See §3. - Ban triggers are dominated by pattern, not by rate. The community evidence is consistent: the same library, same version, same volume gets banned on one account and survives on another. What correlates with bans is (a) cold outbound to non-contacts, (b) group operations, (c) status/story broadcasting, (d) datacenter IPs, (e) headless-browser fingerprint. See §2.
- Channels, story posting, and announcements are the highest-risk features and should not ship in a "normal users" product. Group management is medium-risk. Agent chatbot replying to inbound is lowest-risk. See §1.
- Use the official Cloud API for anything outbound-initiated, and keep Web automation only for inbound-reply automation — or better, migrate the whole product to Cloud API + Tech Provider. Cost is per-message and, crucially, service replies inside the 24h window are free, which fits a "normal users" product almost perfectly. See §4.
- Rate-limit store: use the Postgres you already run (token bucket in a table) as the source of truth, with an in-process cache in front. Redis on the VM adds a second stateful service to a single-VM deployment for no correctness gain, and Upstash adds a network dependency whose failure mode is the worst of the three. See §5.
1. What the Business Messaging Policy actually prohibits, and which planned features risk it
1.1 The policy text (OFFICIAL)
Primary source: WhatsApp Business Messaging Policy, last updated 2026-09-23 — https://business.whatsapp.com/policy (also mirrored at https://www.whatsapp.com/legal/business-policy). Verbatim excerpts that matter here:
- Opt-in is mandatory for any business-initiated contact: "You may only contact people on WhatsApp if: (a) they have given you their mobile phone number or username; and (b) you have received opt-in permission from the recipient confirming that they wish to receive subsequent messages or calls from you."
- No impersonation / no speaking in another's voice: "We may consider your account to be violating this Business Messaging Policy … if you pretend to take on someone else's name, brand, or speak in the voice of another business or entity without permission."
- Platform-specific: "You may only initiate conversations using an approved Message Template … Outside the 24-hour customer service window, you may only send messages via approved Message Templates."
- Automation is allowed but must have a human escape hatch: "You may use automation when responding during the 24-hour window, but must also have available prompt, clear, and direct escalation paths." (In-chat human agent transfer, phone, email, web support, in-store, or support form.)
- No data reuse beyond the thread: "Don't use any data obtained from us about a person you message within WhatsApp, other than the content of message threads, for any purpose other than as reasonably necessary to support messaging with that person."
- Prohibited verticals (Section 4/5) include firearms, alcohol/tobacco, drugs, medical/healthcare products, currency/ICO/binary options, real-money gambling & gaming, adult products and services, dating services, multi-level marketing, and payday loans / debt collection / bail bonds — with narrow, country-gated exceptions on the Platform only, never on the Business App.
Companion document — WhatsApp Messaging Guidelines, last updated 2026-09-23 — https://www.whatsapp.com/legal/messaging-guidelines. This is the clause that governs the Web automation case specifically:
"Do not engage in adversarial behavior. This includes efforts to undermine WhatsApp integrity systems, abuse reporting tools, compromise user accounts, scrape data, or use unofficial clients, bulk messaging, auto-messaging, auto-dialing, or automation to harm WhatsApp or our users."
And the enforcement mechanism, same page:
"WhatsApp may use both automated processing techniques and human review teams to detect and review information available to us … Automated data processing is central to our review process and automates decisions for certain areas where account behavior or reported message content is highly likely to be in violation."
Actions available to WhatsApp (same page): warning, "Temporarily or permanently suspending an account", "Preventing further activity in chats, groups and/or communities", revoking invite links, reporting to authorities.
1.2 Feature-by-feature risk table
Risk is scored for a "normal users" product (small businesses / individuals using their own number), not for an enterprise with a verified WABA.
| Feature | Policy exposure | Ban-risk (Web automation) | Verdict |
|---|---|---|---|
| Agent chatbot (replies to inbound) | Low. Explicitly permitted "when responding during the 24-hour window" with escalation paths. | Low — replying to a user who wrote first is the single safest pattern in every community thread. | Ship. Keep escalation path (human handoff) mandatory. |
| Contact import from CSV/VCF/Excel | Medium. Import itself is not prohibited; messaging those contacts without opt-in is. Policy requires opt-in "confirming that they wish to receive subsequent messages". | Medium–High — an imported list is by definition mostly cold contacts; the first outbound to each is a cold reachout. | Ship import, but gate outbound. Import must not imply permission to message. No "import then blast". |
| Group management (create, add/remove participants) | Medium. Groups are governed by the Messaging Guidelines; no explicit ban, but "preventing further activity in … groups" is an enforcement action. | High — multiple independent community reports tie group operations to bans (see §2.3). | Restrict. Read-only group info is fine; mutation (add/remove/kick) is the risky part. |
| Announcements (one-to-many to a list) | High. This is business-initiated messaging outside a service window → requires approved templates on the Platform, and is "bulk messaging" on the app. | Very High — this is the canonical ban pattern. | Do not ship on Web automation. Platform-only, template-gated, opt-in-gated. |
| Channels / Newsletter | Medium–High. A channel is a broadcast surface; Meta requires channels to follow the Messaging Guidelines and has removed channels for policy violations. | High — community reports of bans for automated channel/status activity. | Do not ship in a "normal users" product. |
| Story / Status posting | Medium. Status is a normal user feature, but automated status posting at scale is "auto-messaging". | Very High — the single most-cited ban trigger in the Baileys corpus (§2.3). | Do not ship. |
| @mentions | Low. Mentions are just message content. | Low — no independent ban signal. | Ship. |
| Chat workflows (n8n automation) | Medium. Automation is allowed inside the 24h window with escalation; automation that initiates is not. | Medium — risk is entirely in what the workflow does, not that it exists. | Ship, but constrain triggers. Allow inbound-triggered; block outbound-initiated. |
| n8n automation (generic) | Same as above. | Medium | Same as above. |
1.3 The one-line rule that resolves the table
Inbound-triggered automation is compliant and low-ban-risk. Outbound-initiated automation is where both the policy and the ban risk live.
Every feature above sorts cleanly along that axis. The product's own docs/04-e2e-encryption.md §4.4 ("No bulk send", single-message endpoints only) is the correct instinct — but it is necessary, not sufficient, because a "single-message endpoint" can still be called in a loop to cold contacts. The constraint has to be on the pattern (cold vs. warm, inbound vs. outbound), not on the API shape.
2. Real-world ban triggers for unofficial Web automation
2.1 What Meta says officially (OFFICIAL)
Meta does not publish a ban-trigger list for unofficial clients — there is no "here is what gets you banned" page, and that absence is itself a finding. What Meta publishes is:
- The Messaging Guidelines clause quoted in §1.1 (unofficial clients / bulk / auto-messaging = adversarial behaviour).
- The enforcement description: automated processing + human review, with suspension as an available action.
- On the official Platform, quality rating and template pacing are the enforcement levers — https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-quality. These do not apply to Web automation, which is the point: on Web automation there is no published quality system, only opaque enforcement.
2.2 What is known (COMMUNITY, high confidence — repeated across independent threads)
These are reported consistently enough across independent accounts, versions, and years that they should be treated as real signals:
- Headless-browser fingerprint.
whatsapp-web.jsdrives Chrome via Puppeteer. A widely-cited vendor analysis documentsnavigator.webdriver === true, WebGL renderer strings that differ between a headless cloud instance and a consumer laptop, and machine-regular timing as detection vectors — https://whapi.cloud/blog/browser-use-whatsapp-automation. This is a vendor blog with a commercial interest in the conclusion; treat the mechanism as plausible-but-vendor-framed, and note thenavigator.webdriverproperty is a real, documented browser API. - Datacenter IP reputation. Multiple community sources assert VPS/datacenter IPs are scored worse than residential/4G. Example:
baileys-antibanREADME — "WhatsApp's ban detection includes IP reputation scoring. Datacenter IPs (VPS) are flagged." — https://github.com/kobie3717/baileys-antiban. UNVERIFIED as policy; plausible as implementation. No primary source. - Cold outbound to non-contacts. The clearest single signal. Baileys issue #1983: "Send the messages to non saved contacts which has no past chats and in 15-20 such messages in a day your number get restricted" — https://github.com/WhiskeySockets/Baileys/issues/1983.
- Group operations. Baileys issue #1869, a long multi-user thread: "one of my accounts used group functions too much (such as opening or closing groups, joining groups, removing group members, etc.) and ended up getting temporarily banned … until the third time before I was permanently banned. That's why I've temporarily disabled functions related to managing groups." — https://github.com/WhiskeySockets/Baileys/issues/1869.
- Status/story broadcasting. Baileys issue #2309, titled "Account gets permanently banned when uploading WhatsApp status using Baileys on production server": 2,000–3,000 contacts, status uploaded in chunks of 100 → temporary ban twice → permanent ban — https://github.com/WhiskeySockets/Baileys/issues/2309.
- Bulk number-existence probing.
whatsapp-web.jsissue #3948: repeatedclient.isRegisteredUser()/getNumberId()calls → number marked as spam — https://github.com/pedroslopez/whatsapp-web.js/issues/3948. This is directly relevant: any "validate my imported contact list" feature is a ban trigger. - The blunt version, from a maintainer-adjacent reply in
whatsapp-web.js#3594: "This happens because WhatsApp can automatically detect bots that run 24/7 … any bot that doesn't use the official WhatsApp Business API … is at risk of being flagged or banned." — https://github.com/wwebjs/whatsapp-web.js/issues/3594.
2.3 What is folklore (COMMUNITY, low confidence — mark as folklore)
These appear in threads but have no consistent supporting evidence and should not drive design:
- "Updating Baileys to the latest version avoids bans." Folklore. The #1869 thread explicitly shows users banned on rc.3, rc.4, rc.5, and v6.7.19 simultaneously.
- "Staying on an old version avoids bans." Also folklore, and contradicts the above. Both claims appear in the same thread.
- "Proxy rotation solves it." Partially true at best; the browser-fingerprint analysis argues proxies do not fix a fingerprint problem. Treat as folklore for the browser engine, plausible for the socket engine.
- "A 7-day warm-up guarantees safety." Folklore as a guarantee. Warm-up is directionally right (see §3) but no source shows it is sufficient.
- "WhatsApp bans non-bot accounts too, so nothing helps." Appears in #1869. Possibly true as a base rate, but it is not actionable and is often used to justify risky behaviour. Do not use it to relax limits.
- Specific numeric thresholds ("8 msg/min is safe", "200/hour max", ">100 msgs/day on new numbers = ban"). These circulate in READMEs and blog posts (e.g. the
baileys-antibanREADME suggests "~8 messages/minute, 200/hour max"). UNVERIFIED. No primary source; treat as a starting hypothesis to be measured, not a spec.
2.4 The honest uncertainty statement
We do not know the ban trigger. Meta does not publish it, and the community corpus is confounded: the same code and volume produces bans on some accounts and not others (#1869: "two bots had been using Baileys for over 3 years and had never been banned, and now they're banned"). The defensible engineering position is therefore:
Treat every account as if it can be banned at any time, design for graceful account loss (per-tenant session isolation, fast re-pairing, clear user messaging), and constrain behaviour to the known-safe pattern (inbound reply, warm contacts, human-like pacing) rather than trying to find a safe rate.
3. Defensible rate limits and pacing — is 60 msg/min safe, reckless, or conservative?
3.1 The comparison that matters
The planned default is 60 msg/min per tenant (docs/04-e2e-encryption.md §4.4). Put it next to the only official numbers that exist:
| Reference | Rate | Source |
|---|---|---|
| Official Cloud API throughput, default | 80 messages/second per business phone number | OFFICIAL — https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput/ |
| Official Cloud API, Business-App-coexistence numbers | 20 msg/s | OFFICIAL — same page |
| Official Cloud API, per-user brake | "If you attempt to send too many messages to the same WhatsApp user number, you may encounter a pair rate limit error." | OFFICIAL — same page |
| Planned BFF default | 60 msg/min = 1 msg/s | this repo |
| OpenWA's own monitoring target | warn >15 msg/min, critical >25 msg/min | OpenWA docs/16-risk-management.md §R007 |
| Community anti-ban suggestion | ~8 msg/min, 200/hour | COMMUNITY / UNVERIFIED — baileys-antiban README |
3.2 The reasoning, shown
-
60 msg/min is below the official throughput ceiling by 80×. So if the question were "does this exceed a published Meta limit?", the answer is no. That is the trap: the official 80 msg/s figure applies to the Cloud API, where Meta has already verified the business, gated it behind template approval, and given it a quality rating. A Web-automation session has none of those gates. Copying the official number's order of magnitude onto an unofficial session is category error.
-
60 msg/min is above OpenWA's own critical threshold (25 msg/min) by 2.4×. The gateway's own risk doc says >25 msg/min is a critical monitoring condition. A product default that sits above its own vendor's critical threshold is not conservative by any reading.
-
60 msg/min is above the community's suggested safe band (~8 msg/min) by 7.5×. Community numbers are unverified, but they are the only field data we have, and they all point down, not up.
-
The number is flat, and flatness is the real defect. A brand-new number that sends 60 msg/min on day one is exactly the pattern OpenWA's own docs call out as the ban trigger: "a brand-new WhatsApp account that immediately sends at volume is the pattern that gets numbers banned" (
OpenWA/docs/06-api-specification.md§Send pacing). A ten-year-old number replying to inbound at 60 msg/min is a different risk class entirely. One number cannot serve both. -
Sustained 60 msg/min is 86,400 messages/day. Even if only a fraction of tenants hit it, the ceiling defines what the product permits, and a per-tenant ceiling of 86k/day is a bulk-messaging product by any reasonable definition — which contradicts the captain's directive ("no heavy bulk and mass send") and §4.4's own "No bulk send" heading.
3.3 Recommendation
Verdict: 60 msg/min is reckless as a default. It is defensible only as a hard ceiling for a warmed, established, inbound-replying account — and even then it should be a ceiling, not a target.
Recommended shape (all per session/tenant, all enforced server-side in the BFF and left enabled in OpenWA as the second layer):
# Layer 1 — warm-up (session age gates the allowance)
day 0–1: no outbound-initiated sends at all; inbound replies only
day 2–7: ≤ 20 outbound-initiated/day, ≤ 1 msg/min
day 8–30: ≤ 100 outbound-initiated/day, ≤ 3 msg/min
day 31+: ≤ 500 outbound-initiated/day, ≤ 6 msg/min
# Layer 2 — cold reachout (the highest-risk class)
cold_reachout_daily_cap: 20/day for any account, regardless of age
(a "cold reachout" = no prior history with that chat in either direction)
# Layer 3 — per-chat pacing (anti-pattern detection)
min_gap_same_chat: 3 s, jittered ±2 s
min_gap_any_chat: 1 s, jittered (never send two chats in the same second)
# Layer 4 — breaker
consecutive_failures >= 3 -> pause session, alert tenant
disconnect/ban-warning -> hard stop, do not auto-reconnect in a loopInbound replies are exempt from Layers 1–2 (they are the compliant, low-risk pattern and should not be throttled into uselessness). They remain subject to Layer 3 pacing and Layer 4 breaker.
Why this shape and not a single number: it separates the three variables that actually drive risk — account age, cold vs. warm, and burstiness — instead of collapsing them into one rate. A single "60 msg/min" knob cannot express "a new number must not cold-message at all", which is the single most important rule.
What to change in the repo:
docs/04-e2e-encryption.md§4.4: change the default from60 msg/minto the layered policy above; keep a hard ceiling of 6 msg/min sustained for outbound-initiated, and note that inbound replies are exempt.- Turn
SEND_PACING_ENABLED=trueon by default in the shipped OpenWA config. It is currently off, which means the gateway's own warm-up and cold-reachout caps do nothing unless an operator finds and flips the flag. Shipping a "normal users" product with the anti-ban layer disabled is the wrong default. - Note the OpenWA caveat: send pacing "counts only sends that write a
messagesrow" and "Clearing or deleting a chat removes its stored rows, so that chat's sends today stop counting" (OpenWA/docs/16-risk-management.md§R002). A tenant can therefore reset their own cold-reachout counter by deleting the chat. The BFF's own counter must not be derived from OpenWA's message table — it needs its own durable ledger. This is a concrete reason the rate-limit store (§5) must be the BFF's, not OpenWA's.
4. Should any feature use the official Cloud API instead?
4.1 Pricing (OFFICIAL)
Primary source: Pricing on the WhatsApp Business Platform, updated 2026-09-30 — https://developers.facebook.com/docs/whatsapp/pricing/. Key facts, verbatim/near-verbatim:
- "Effective July 1, 2025 – Meta charges on a per-message basis … Meta charges by category–market, and only for messages from businesses to users. Meta does not charge for messages from users to businesses."
- "Businesses are only charged when a message is delivered."
- Five categories: Marketing, Utility, Authentication, Service, and (since 2026-07-01) Meta Business Agent.
- Non-template messages can only be sent inside an open 24-hour customer service window. "non-template messages cannot be used to reach out to users."
- Free tier: "For each business phone number, Meta provides 1,000 free service messages per month."
- Free entry point: a Click-to-WhatsApp ad opens a window in which "Meta does not charge for marketing, utility, authentication or service messages" for up to 7 days.
- Rates vary by market. Meta publishes downloadable rate cards (CSV/PDF) rather than inline dollar figures — https://business.whatsapp.com/products/platform-pricing#rates. Morocco (MA, +212) is on the rate card as a standalone market as of 2026-10-01 (it was previously billed at "Rest of Africa" rates). I could not extract the exact MA per-message dollar figure from the rate-card CSV in this session — treat any specific MA number as UNVERIFIED until read off the official CSV.
- Meta Business Agent (the AI-agent category): "One global rate of $2.00 USD per 1 million (M) tokens. One message typically consumes 20,000–25,000 tokens, which translates to approximately 4–5 cents (USD) per message."
4.2 Cost model for this product
The decisive fact for a "normal users" product is: service messages — i.e. replies to a user who messaged you first, inside the 24h window — are free for the first 1,000 per business phone number per month, and cheap after that. A chatbot that only replies to inbound is therefore near-zero marginal cost on the official API.
| Planned feature | Cloud API cost | Verdict |
|---|---|---|
| Agent chatbot (inbound replies) | Free (first 1,000 service msgs/number/month), then low per-market service rate | Migrate to Cloud API. This is the feature where official is both compliant and cheap. |
| Announcements / outbound | Paid per delivered template, marketing category = most expensive | Cloud API only. On Web automation this is a ban; on Cloud API it is a cost. |
| Contact import | Free (import is local) | Keep local; but outbound to imported contacts must go through templates. |
| Group management | Not available on Cloud API (Cloud API has no group API) | Web-automation-only feature → highest-risk, recommend cutting. |
| Channels / stories | Not available on Cloud API | Web-automation-only → recommend cutting. |
| @mentions, chat workflows, n8n | Works on both | Keep on whichever engine; constrain triggers per §1.3. |
4.3 The structural point
Cloud API is not a drop-in replacement — it has no group API, no channels, no stories, no contact-list access, and no free-form outbound. That is not a limitation to work around; it is Meta's compliance boundary expressed as an API surface. The features that Cloud API cannot do are precisely the features this report scores as high ban-risk. That alignment is the strongest argument in this document:
If a feature cannot be built on the official API, that is Meta telling you the feature is outside the compliant envelope. Building it on Web automation is building the thing Meta designed the API to prevent.
4.4 Recommendation
- Phase 1: keep Web automation for inbound-reply automation only (chatbot, @mentions, workflows), with the §3 pacing.
- Phase 2: add Cloud API as a per-tenant engine choice, and make it the default for any tenant that wants outbound/announcements.
- Phase 3: if the product succeeds, become a Tech Provider (§6) and make Cloud API the primary path, with Web automation as a legacy/opt-in mode clearly labelled as unsupported by Meta.
5. Rate-limit store: Redis-on-VM vs. Upstash vs. Postgres token bucket
5.1 The three options
| Redis container on the VM | Upstash Redis | Postgres token bucket | |
|---|---|---|---|
| Where state lives | Same arm64 VM, same host as BFF | Third-party, network hop | The Postgres the BFF already uses |
| Cost | $0 (uses existing VM RAM/CPU) | Free tier: 256 MB, 500K commands/month, 10 GB bandwidth; PAYG $0.20/100K commands; Fixed from $10/mo (250 MB, unlimited commands) — https://upstash.com/pricing/redis | $0 (existing DB) |
| Ops surface | +1 stateful service to run, back up, monitor, upgrade | 0 (managed) | 0 (already operated) |
| Latency | sub-ms (localhost) | ~1–20 ms (network, region-dependent) | ~1–5 ms (localhost) |
| Failure mode when down | Rate limiter unavailable | Rate limiter unavailable and the whole BFF is on a network dependency | Rate limiter unavailable, but DB is already a hard dependency of every request |
| arm64 / 12 GB fit | Fine (Redis is tiny) | N/A | Fine |
5.2 The command-budget arithmetic (why Upstash is a poor fit here)
A token-bucket check is at minimum 1 command per send attempt, realistically 2–3 (read bucket, write bucket, maybe a Lua script call). Take the planned ceiling of 60 msg/min/tenant:
60 msg/min × 60 min × 24 h = 86,400 checks/day/tenant (worst case)At 1 command/check, one tenant at the ceiling burns the entire 500K/month free tier in under 6 days. At 10 tenants it is gone in hours. Upstash's own docs make the point bluntly: "500,000 commands a month sounds like a lot until you divide it: about 16,000 a day, or 11 a minute sustained. A rate limiter fits until traffic arrives." — https://layerbase.com/blog/redis-free-tier-comparison.
So Upstash means either (a) a Fixed plan from $10/mo, or (b) PAYG where cost scales linearly with message volume — i.e. the rate limiter becomes a per-message tax, which is exactly the wrong cost curve for a product whose whole value proposition is cheap messaging. Reject Upstash for the hot path.
5.3 Redis-on-VM vs. Postgres
Redis is the conventional answer, but the conventional answer assumes a fleet of app servers sharing state. This is one VM. The BFF is a single process (or a handful of workers) on the same host. That changes the analysis:
- Redis's advantage (shared, fast, atomic counters across processes) is mostly wasted when there is one host and the BFF can keep an in-process cache.
- Redis's cost (a second stateful service on a single-VM deployment) is real: another thing to back up, another thing to lose on reboot, another thing whose persistence mode (
appendonlyvs.rdb) you must reason about. On a single VM, "we lost Redis on restart" and "we lost the rate limiter" are the same event. - Postgres is already a hard dependency. Every BFF request already touches Supabase Postgres (per
docs/04-e2e-encryption.md§4.2). If Postgres is down, the BFF is down regardless of the rate limiter. So putting the bucket in Postgres does not add a new failure mode — it reuses one that already exists.
5.4 Recommendation
Use a Postgres-backed token bucket as the source of truth, with an in-process (Bun KV / in-memory) cache in front of it for the hot path.
Design:
CREATE TABLE rate_buckets (
tenant_id UUID NOT NULL,
bucket_key TEXT NOT NULL, -- e.g. 'outbound', 'cold', 'chat:<jid_hash>'
tokens NUMERIC NOT NULL,
last_refill_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (tenant_id, bucket_key)
);- Hot path: read/update the bucket from the in-process cache. Refill is computed from elapsed time (lazy refill, no background job).
- Durability: flush to Postgres on a short interval (e.g. 1 s) and on every refusal. A refusal must be durable — a lost refusal means a burst gets through.
- Atomicity:
UPDATE … SET tokens = … WHERE tokens >= cost RETURNING tokens— the row lock is the atomicity, no Lua needed. - This is also the counter OpenWA cannot provide (§3.3): the BFF owns its own ledger, so a tenant deleting a chat cannot reset their cold-reachout count.
5.5 Failure mode: fail open or fail closed?
This is the question that actually decides the design, and the answer differs by bucket:
| Bucket | Store down → | Why |
|---|---|---|
| Cold-reachout / outbound-initiated | FAIL CLOSED (refuse the send) | This is the ban-risk bucket. Letting a burst through because the limiter is down is how you lose a tenant's account. A refused message is recoverable; a banned number is not. |
| Inbound replies (service) | FAIL OPEN (allow) | Replying to a user who wrote first is the compliant, low-risk pattern. Blocking it because Postgres hiccuped breaks the product for no safety gain. |
| Per-chat pacing | FAIL CLOSED for the same chat (refuse), FAIL OPEN across chats | Same-chat burst is a risk signal; cross-chat is not. |
Why not fail-open everywhere (the usual default)? Because the asymmetry of consequences is extreme: a rate limiter that fails open under load is precisely the scenario that produces the burst that produces the ban. The cost of failing closed on outbound is a delayed message; the cost of failing open is a dead phone number. Fail closed on the risky bucket.
Why not fail-closed everywhere? Because it would make Postgres a hard gate on the safe path, turning a transient DB blip into a product outage for the feature that carries no ban risk. That is the mistake that makes teams disable rate limiting entirely "just to get through the incident".
Concretely: if the store is unreachable, the BFF should (a) allow inbound replies, (b) refuse outbound-initiated sends with a clear 429-style error and a retry hint, (c) log loudly, (d) never silently degrade to "no limit". This is the same contract OpenWA already uses for its own pacing (429 SEND_PACING_LIMITED with retryAfterSeconds, per OpenWA/docs/06-api-specification.md), so the BFF's behaviour will be consistent with the layer beneath it.
6. Legal/ToS: does reselling multi-tenant WhatsApp access require being a BSP or Meta partner?
This is the most consequential finding in this report, and the answer is not "yes, get a BSP" — it is "the thing you are describing is prohibited by two separate WhatsApp legal documents."
6.1 The clauses (OFFICIAL, quoted verbatim)
A. WhatsApp Business App Terms — https://www.whatsapp.com/legal/WhatsApp-Terms-for-WhatsApp-Business-App, §5 "Restrictions":
"Except as otherwise permitted by WhatsApp in writing, Company must not directly, indirectly, or through automated or other means: (a) distribute, sell, resell, or rent our Business App Services to third parties; (b) distribute or make our Business App Services available over a network to be used by multiple devices at the same time, except as authorized through tools and configurations that we have expressly provided for your use via our Business App Services; and (c) copy, reproduce, distribute, publicly perform or display, modify, or make derivative works based upon all or portions of our Business App Services."
and further:
"(g) develop or use any applications that interact with our Business App Services without our prior written consent; and (h) create software or APIs that function substantially the same as our Business App Services and offer them for use by third parties in an unauthorized manner on or utilizing the WhatsApp platform or our Business App Services."
B. WhatsApp Terms of Service (consumer) — https://www.whatsapp.com/legal/terms-of-service, "Harm To WhatsApp Or Our Users":
"you must not directly or through automated means: … (g) sell, resell, rent, or charge for our Services or data obtained from us or our Services in an unauthorized manner; (h) distribute or make our Services available over a network where they could be used by multiple devices at the same time, except as authorized through tools we have expressly provided via our Services …"
C. WhatsApp Terms for the Business Platform — https://www.whatsapp.com/legal/WhatsApp-Terms-for-WhatsApp-Business-Platform, §3 "Account":
"You represent and warrant that you: (a) will use our Platform Services solely for business, commercial, and authorized purposes, and not for personal use; (b) will only provide account information associated with your Company; and (c) have not been previously suspended or removed from our Platform Services…"
6.2 What this means, plainly
-
A multi-tenant gateway that lets many end-customers use WhatsApp through your infrastructure is, on its face, "making our Services available over a network to be used by multiple devices at the same time" and "reselling access". Both the Business App Terms (a)+(b) and the consumer ToS (g)+(h) prohibit this without WhatsApp's prior written consent. This is true regardless of whether you use the official API or Web automation — the Business Platform Terms (C) additionally require you to only provide account info "associated with your Company", which is the opposite of multi-tenant.
-
The escape hatch is real and it is the partner program. The Business App Terms say "Except as otherwise permitted by WhatsApp in writing"; the Business Platform Terms are the "otherwise". Meta's partner program is the mechanism by which written permission is granted. So the honest answer to "does reselling require being a BSP or Meta partner?" is:
Yes. Reselling multi-tenant WhatsApp access requires written authorisation from Meta, and the only published route to that authorisation is the partner program — Tech Provider → Tech Partner, or Solution Partner (the entity formerly called BSP). Operating a multi-tenant reseller gateway without it is a breach of the Business App Terms §5(a)/(b)/(g)/(h) and the consumer ToS.
-
"BSP" is now "Solution Partner". Meta's current partner taxonomy is two types — Tech Partners (third-party developers building on the Platform) and Solution Partners (end-to-end solutions, and "Only Solution Partners can extend a line of credit to businesses for payment") — https://business.whatsapp.com/partners/become-a-partner. The old "BSP" label maps to Solution Partner. A Tech Provider is the entry tier: "Third party developers must onboard first as a Tech Provider before upgrading to Tech Partner."
-
What Tech Provider requires (OFFICIAL) — https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/get-started-for-tech-providers:
- A Meta app with the WhatsApp use case + connected business portfolio.
- Business verification with Meta.
- App Review granting Advanced access to
whatsapp_business_messaging(send on behalf of clients) andwhatsapp_business_management(access clients' WABAs). "Without it, API calls that use this permission on accounts not owned by your business return error code 200." - Webhooks and billing configured; onboarded clients add a credit card.
- Tech Partner upgrade additionally requires "greater than or equal to 2,500 average daily messages (sent or received) … over the last 7 days" — https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/upgrade-to-tech-partner/.
-
The billing model differs by tier, and it matters commercially. Tech Providers keep the billing relationship with Meta (clients pay Meta directly, no markup); Solution Partners hold a line of credit and bill the client, typically with a markup — https://whautomate.com/whatsapp-tech-provider-vs-bsp (COMMUNITY/vendor, but consistent with Meta's own partner page). So "reseller with markup" ≈ Solution Partner; "software on top of the API" ≈ Tech Provider.
6.3 The uncomfortable conclusion for this product
The product as scoped — OpenWA wrapping WhatsApp Web, multi-tenant, white-labelled, reselling access to normal users — is not a grey area. It is:
- A ToS breach on the Web-automation path (Business App Terms §5(a)/(b)/(g)/(h); consumer ToS (g)/(h); Messaging Guidelines "unofficial clients").
- A ToS breach on the official-API path too, unless the operator is a Tech Provider/Solution Partner, because the Business Platform Terms require account info "associated with your Company" and forbid personal use.
There is no configuration of rate limits, engines, or features that fixes this. The only compliant versions of the product are:
- (i) A Tech Provider that onboards each tenant into their own WABA via Embedded Signup, so the tenant owns the account and the provider merely operates software on their behalf. This is the legitimate shape of a multi-tenant WhatsApp SaaS. It requires business verification, App Review, and accepting Meta's Tech Provider terms.
- (ii) A Solution Partner (formerly BSP) that resells with a line of credit and markup.
- (iii) A single-tenant deployment where each customer runs their own OpenWA on their own infrastructure and the vendor ships software, not access. This is the OpenWA open-source model, and it is why OpenWA's README carries the disclaimer: "OpenWA is an independent open-source project and is not affiliated … Use it at your own risk; you are responsible for complying with WhatsApp's terms and the law where you operate." Note that even here, the customer is the one taking on the ToS risk, and the vendor must not be operating the sessions.
Recommendation: treat the multi-tenant reseller model as blocked pending a Tech Provider application, and get legal review before shipping. This is a business-model decision, not an engineering one, and it should be escalated to the captain explicitly. The engineering work in §3 and §5 is still worth doing — it is necessary for whichever model is chosen — but it does not make the current model compliant.
7. Source list
OFFICIAL (Meta/WhatsApp), fetched 2026-10-08:
- WhatsApp Business Messaging Policy — https://business.whatsapp.com/policy · https://www.whatsapp.com/legal/business-policy (updated 2026-09-23)
- WhatsApp Messaging Guidelines — https://www.whatsapp.com/legal/messaging-guidelines (updated 2026-09-23)
- WhatsApp Terms of Service (consumer) — https://www.whatsapp.com/legal/terms-of-service
- WhatsApp Terms for WhatsApp Business App — https://www.whatsapp.com/legal/WhatsApp-Terms-for-WhatsApp-Business-App
- WhatsApp Terms for WhatsApp Business Platform — https://www.whatsapp.com/legal/WhatsApp-Terms-for-WhatsApp-Business-Platform
- Pricing on the WhatsApp Business Platform — https://developers.facebook.com/docs/whatsapp/pricing/ (updated 2026-09-30)
- Rate cards & volume tiers — https://business.whatsapp.com/products/platform-pricing#rates
- Messaging Limits — https://developers.facebook.com/docs/whatsapp/messaging-limits/
- Throughput — https://developers.facebook.com/documentation/business-messaging/whatsapp/throughput/
- Become a Tech Provider — https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/get-started-for-tech-providers
- Upgrading to a Tech Partner — https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/upgrade-to-tech-partner/
- Become a Partner (taxonomy) — https://business.whatsapp.com/partners/become-a-partner
- Template quality rating — https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/template-quality
COMMUNITY (observed behaviour, not policy):
whatsapp-web.js#3594 — https://github.com/wwebjs/whatsapp-web.js/issues/3594whatsapp-web.js#3565 — https://github.com/wwebjs/whatsapp-web.js/issues/3565whatsapp-web.js#2778 — https://github.com/pedroslopez/whatsapp-web.js/issues/2778whatsapp-web.js#3948 (number probing) — https://github.com/pedroslopez/whatsapp-web.js/issues/3948- Baileys #1850 — https://github.com/WhiskeySockets/Baileys/issues/1850
- Baileys #1869 (group ops, version folklore) — https://github.com/WhiskeySockets/Baileys/issues/1869
- Baileys #1983 (cold outbound) — https://github.com/WhiskeySockets/Baileys/issues/1983
- Baileys #2309 (status broadcasting) — https://github.com/WhiskeySockets/Baileys/issues/2309
baileys-antibanREADME (anti-ban middleware, IP reputation claim) — https://github.com/kobie3717/baileys-antiban- Whapi.Cloud browser-fingerprint analysis (vendor, commercial interest) — https://whapi.cloud/blog/browser-use-whatsapp-automation
COMMUNITY / VENDOR (pricing & partner model):
- Upstash Redis pricing — https://upstash.com/pricing/redis · https://upstash.com/docs/redis/overall/billing
- Redis free-tier command-budget analysis — https://layerbase.com/blog/redis-free-tier-comparison
- Tech Provider vs. BSP billing model — https://whautomate.com/whatsapp-tech-provider-vs-bsp
THIS REPO:
docs/04-e2e-encryption.md§4.4 (No bulk send; 60 msg/min default)OpenWA/docs/16-risk-management.md§R002, §R007 (ban risk, pacing defaults, monitoring thresholds)OpenWA/docs/06-api-specification.md§Send pacing (opt-in, off by default)OpenWA/docs/01-project-overview.md§1.5 (feature scope: groups, channels, status/stories)OpenWA/README.md(disclaimer)
8. Open questions / UNVERIFIED items
- Exact Cloud API per-message rates for Morocco (MA). MA became a standalone rate-card market on 2026-10-01; the dollar figures live in Meta's downloadable CSV, which I did not parse in this session. UNVERIFIED — read the CSV before building a cost model.
- Whether WhatsApp's ban detection actually uses IP reputation,
navigator.webdriver, or WebGL fingerprinting. Asserted by vendors and community; no primary source. UNVERIFIED mechanism, but the outcome (bans for unofficial clients) is official policy and well-attested. - Any specific "safe" rate. No primary source exists. The §3 numbers are a reasoned starting policy, not a measured safe rate. They should be treated as hypotheses and instrumented (track ban/restriction events per tenant against their pacing profile) so the policy can be tuned from data.
- Whether a Tech Provider operating on behalf of tenants fully cures the multi-tenancy problem. Meta's docs describe the Tech Provider flow (Embedded Signup, per-client WABA, advanced permissions) and it is the intended mechanism, but I did not find an explicit statement that it authorises the white-label reseller framing. Get legal review. This is the single highest-value follow-up.