Messaging & the anti-ban policy
The caps, recipient classes and error codes evaluateSendPolicy enforces on every WhatsApp send.
Cet article n'a pas été traduit, il est donc affiché en anglais.
Messaging & the anti-ban policy
A shop's WhatsApp is a real phone number on WhatsApp Web; WhatsApp bans numbers that behave like spam. evaluateSendPolicy (src/libs/services/WhatsAppSendPolicy.ts) refuses on the tenant's behalf before anything is sent, on every route that can put a message on the wire — not only the API.
Recipient classes
Every 1:1 send is classified before the caps below are checked:
| Class | Meaning ||---|---|| known | matches a CustomerModel row, or the number has written to the shop before || declared | not known, but the tenant has declared it via POST …/contacts/consented and the declaration is still active (not revoked, not expired) || cold | neither of the above — a genuine stranger || group | the recipient is a WhatsApp group (@g.us), never known/declared/cold |Caps (1:1)
| Rule | Default | Code ||---|---|---|| Content | ≤ 4096 chars, ≤ 1 media, text or media required | MESSAGE_TOO_LONG / TOO_MANY_MEDIA / EMPTY_MESSAGE || Opt-out | recipient's last inbound was STOP/ARRÊT/DÉSABONNER/UNSUBSCRIBE | RECIPIENT_OPTED_OUT || Warm-up ramp (days since the WhatsApp session connected) | d0–2: 20/day · d3–6: 50 · d7–13: 100 · d14+: 250 | DAILY_CAP_REACHED || Hourly | max(10, daily ÷ 6) | HOURLY_CAP_REACHED || Cold recipients | 10% of the daily cap, minimum 5 | COLD_RECIPIENT_CAP_REACHED || Declared recipients | its own, tenant-configurable budget — separate from cold's, does not spill over into it | DECLARED_RECIPIENT_CAP_REACHED || Per recipient | ≤ 3 messages / 24h, ≥ 10 min apart unless they replied since | RECIPIENT_RATE_LIMITED || Bulk pattern | identical text to more than 20 distinct numbers within 1h | BULK_PATTERN_BLOCKED |Tenants can tune coldPerDay/declaredPerDay and related knobs within a platform-configured floor and roof from Settings → Messaging; a value outside [floor, roof] is rejected server-side, never merely clamped silently.
Caps (groups)
Sending to a group uses an entirely separate budget from the 1:1 one, keyed on groupsPerDay/groupPerRecipient24h, because a group is not a person:
| Rule | Code ||---|---|| The caller must supply group context (participant count, admin status, announce flag) | GROUP_CONTEXT_REQUIRED || The shop's number must administer the group — required for every group, not only "announce" ones, since opt-out cannot work per-member and mere membership is not consent to broadcast | GROUP_NOT_ADMINISTERED || Weighted daily cap: one "share" of groupsPerDay per GROUP_SIZE_UNIT participants (rounded up, minimum 1) — a 200-member group costs more than a 5-member one, because it reaches more people | GROUP_DAILY_CAP_REACHED || Per group | ≤ groupPerRecipient24h messages / 24h | RECIPIENT_RATE_LIMITED |GET /shops/{idOrName}/groups returns isAdmin/announce per group so a caller can filter and explain a likely denial before attempting a send — but the policy above is what is actually authoritative; the route's canSend field is advisory.
Enforced inside the bot regardless of caller
The recipient must exist on WhatsApp (rejected, code RECIPIENT_NOT_ON_WHATSAPP); one send at a time per shop with a random 6–20 second gap between sends; a "composing" (typing) presence shown before each message; a 15-minute pause of the shop's whole queue on a rate signal from WhatsApp itself; and on a lost session every queued message fails WHATSAPP_SESSION_LOST and the channel is flagged until reconnected. If a shop's own WhatsApp is disconnected, some sends may route through the platform's shared pool instead, wrapped with a visible "sent via OmniManager" attribution — never silently as the tenant.
Best practice: message people who expect it, vary wording, spread sends over the day, and honour opt-outs and replies immediately.