WhatsApp Bulk Messaging Guide: Automated, Scheduled and Ban-Free — Every Detail, Start to Finish
You want to send bulk, automated and scheduled WhatsApp messages to your customers, members or donors. There are four ways to do it, each with different costs, limits and risks. This guide first compares all the methods, clarifies the legal framework (consent, GDPR, opt-out), and provides ready-to-use message templates and timing strategies. Then it walks through an open-source system that pairs via QR code, actively protects your number from bans, and runs entirely on your own server — from architecture to troubleshooting, in full detail.
1. What is this system, and who is it for?
This is a self-hosted, open-source application that lets you manage automated, bulk and scheduled WhatsApp message delivery from a single dashboard. Its technical foundation rests on four components: whatsapp-web.js for browser automation, BullMQ + Redis for the job queue, Supabase (PostgreSQL) for the data layer, and React for the admin panel. The whole stack comes up with a single Docker Compose command.
Who is it a good fit for?
- Nonprofits and charities: donation receipts, campaign announcements, seasonal program updates for donors.
- Small and medium businesses: order status, appointment reminders, shipping notifications, promotions.
- Community and HOA managers: member announcements, dues reminders, event notifications.
- Schools and course providers: class schedules, parent updates, enrollment reminders.
- Developers: anyone who wants to trigger WhatsApp notifications from their own application over a simple HTTP API.
Important ethical and legal note: This system is designed for messaging people who have agreed to hear from you. Sending unsolicited messages to purchased phone lists violates both WhatsApp's Terms of Service and data-protection law (GDPR and national equivalents), and it will get your number permanently banned. See the legal framework section for details.
2. The 4 ways to send bulk WhatsApp messages
Most articles pitch you a single product; the right decision comes from knowing the limits of all four methods:
Method 1 — WhatsApp's built-in broadcast lists
Free in both regular WhatsApp and WhatsApp Business. Its two biggest constraints: a maximum of 256 recipients per list, and messages only reach people who have saved your number in their contacts. Enough for a small, loyal audience; there is no scheduling, no reporting, no automation.
Method 2 — The WhatsApp Business app
The free Business app adds a catalog, quick replies and labels — but for bulk sending it is still bound by the 256-recipient broadcast limit and the contact requirement. There is no automation API; everything is manual.
Method 3 — The official WhatsApp Business API (Cloud API)
Meta's enterprise solution. It scales to tens of thousands of messages per day, has zero ban risk and official support. In exchange: Meta Business verification, usually a BSP (solution provider) intermediary, pre-approved template requirements for marketing messages, and per-conversation pricing. Once BSP platform fees are added, mid-volume usage easily reaches significant monthly costs.
Method 4 — QR-based automation (this system)
Pairs the account on your phone via the WhatsApp Web protocol and automates sending. No application process, no template approval, no message fees, no 256 limit, no contact requirement. The price: it's a gray area WhatsApp doesn't officially support, and the ban risk is real without rate discipline — which is why the heart of this system is its anti-ban regime.
| Criterion | Broadcast list | Business app | Official API | QR-based (this system) |
|---|---|---|---|---|
| Recipient limit | 256 / list | 256 / list | None (tiered limits) | None (rate-limited) |
| Contact requirement | Yes | Yes | No | No |
| Message fees | None | None | Per conversation | None |
| Scheduled sending | No | No | Depends on BSP panel | Yes, built-in |
| Free-form content | Yes | Yes | Template approval required | Yes |
| Automation / API | No | No | Yes | Yes (HTTP API) |
| Reporting | No | Limited | Yes | Yes (panel + DB) |
| Daily capacity | Manual | Manual | 10,000+ | ~450–500 (human speed) |
| Ban risk | Low | Low | None | Real; managed with discipline |
| Data control | — | — | With BSP/Meta | Fully yours (self-hosted) |
Decision summary: Under 50 messages a day to a loyal audience → broadcast lists suffice. Thousands per day with enterprise SLAs → the official API. A few hundred notifications a day, free-form content, zero message costs and full data ownership → the QR-based system this guide covers.
3. Cost analysis: official API vs. self-hosted
The official API has two cost lines: Meta's per-conversation fee (varies by category and country; marketing conversations are the most expensive) and the BSP's monthly platform/package fee. For a business sending 5,000–10,000 marketing messages a month, the total bill easily reaches three or four figures in USD.
This system's cost table is flat and volume-independent:
| Item | Cost | Note |
|---|---|---|
| Software | $0 | Open source |
| Server (2 GB RAM VPS) | ~$5–10/month | 2 GB recommended for Chromium |
| Supabase | $0 | The free tier easily covers this volume |
| Per-message fee | $0 | Regardless of volume |
| Template approval / application | $0 / none | Free-form content |
Even at 10,000 notifications a month, your total cost is the server rent. In exchange, your scale ceiling is human speed (~450–500 messages/day), and the responsibility of managing ban risk with discipline is yours.
4. Legal framework: consent, commercial messages and GDPR
This section is not legal advice, but the framework you need to know before bulk messaging is clear:
4.1 Explicit consent
Sending marketing messages to a phone number — personal data — requires valid consent in virtually every jurisdiction (GDPR in the EU, KVKK in Türkiye, TCPA-adjacent rules in the US). Consent must be freely given, informed and specific. A pre-ticked checkbox buried in a signup form does not qualify. Be able to prove when, how and with what wording you obtained it.
4.2 Marketing vs. transactional messages
Promotions, campaigns and greetings are commercial messages and require prior opt-in. Transactional notifications — order status, appointment reminders, delivery updates — are generally treated differently and may fall under exemptions, since the recipient requested the underlying service. Draw this line correctly in your own flows: a shipping notice is not a license to append a promo paragraph.
4.3 The right to opt out
Every commercial message must offer an easy way out. On WhatsApp the practical form is a single closing line: "Reply STOP if you no longer wish to receive these messages." Remove opt-outs from your list immediately — for both legal and ban reasons: the rate at which recipients block or report you is the heaviest signal in WhatsApp's ban decision.
4.4 GDPR specifics
If your list contains EU residents, GDPR applies in full: a valid legal basis (usually consent for marketing), honoring deletion requests, and records of processing. This system's self-hosted nature is an advantage here: data is written to your own server and your own Supabase project rather than a third-party messaging broker, keeping your processor chain short.
Practical summary: (1) Only add people whose consent you hold, and document it. (2) Separate campaigns from transactional notices. (3) Add an opt-out line to every campaign message. (4) Remove opt-outs the same day. A sender who follows these four rules sits in the safest zone both legally and ban-wise.
5. System architecture: web, server, worker, Redis, Supabase
The system consists of four Docker services plus a cloud database. The division of responsibility is deliberately strict — each component does exactly one job:
web (React/nginx :8080) ──► server (Express :3200) ──► Redis (BullMQ 'whatsapp')
│ │
│ HTTP control (:3100) ▼
└──────────────► whatsapp-worker (Node 20 + Chromium)
│
Supabase (notification_messages, whatsapp_web_sessions)
5.1 Web panel (React + nginx)
The only surface the user sees. Four main screens: Dashboard (daily statistics), Compose (creating bulk/scheduled sends), Messages (status tracking and cancellation) and Connection (QR pairing and session status). nginx serves the static files and proxies /api/ requests to the internal server; this is the only externally exposed port.
5.2 Server (Express)
The API layer — and it never sends a message itself. Its jobs: validate incoming dispatch requests, write a queued record for every recipient, enqueue jobs into BullMQ with round-robin priority, and bridge to the worker's control API for QR/session operations. Round-robin priority means a 500-recipient bulk send won't make a single urgent message that arrives later wait for hours.
5.3 Worker (Node 20 + Chromium)
The system's only sending authority. It runs a real WhatsApp Web session inside headless Chromium, consumes the queue one job at a time (concurrency 1), and applies all anti-ban timing internally. The session identity is persisted with LocalAuth in the /sessions volume; even after a container restart you never re-scan the QR. The control API (port 3100) is reachable only from the internal network, authenticated with WA_CONTROL_SECRET.
5.4 Redis (BullMQ)
Where the job queue lives. AOF (Append-Only File) persistence is enabled, so scheduled jobs survive Redis restarts. Scheduled messages sleep here as delayed jobs.
5.5 Supabase (PostgreSQL)
The permanent ledger. The notification_messages table tracks each message's lifecycle, while whatsapp_web_sessions tracks session state. The panel reads all lists and statistics from here. Access happens server-side with the service-role key; no database credential ever reaches the browser.
Why does this separation matter? Because the "enqueuer" (server) and the "sender" (worker) are fully decoupled, rate limiting lives in exactly one place: the worker. Even if the API receives 100 requests per second, traffic toward WhatsApp can never exceed human speed. This is the architectural foundation of the anti-ban guarantee.
6. The anti-ban regime: rules that protect your number
On QR-paired accounts, WhatsApp detects spam through three primary signals: speed (superhuman pace), timing (bulk messages in the middle of the night) and repetition (rapid-fire messages to the same person). A fourth signal joins them: complaint rate — how often recipients block or report you. The system manages the first three; the fourth depends on the quality of your list.
The regime's rules — and, crucially, every value is hard-coded:
| Rule | Value (text) | Value (media) | What it prevents |
|---|---|---|---|
| Random delay between messages | 45–90 s | 67–135 s | The machine-like, fixed-tempo sending signature |
| Burst pause | 8–12 min every 15 messages | 16–24 min every 15 messages | Uninterrupted marathon sending sessions |
| Hour window | 05:00–23:00 (local time) | Night sending — the strongest spam signal | |
| Recipient cooldown | 4-hour repeat block per number | Rapid-fire bombardment of one person | |
| Effective rate | ~25–30 messages/hour | Total volume exceeding human limits | |
Why aren't these values configurable?
Because the moment they are configurable, someone loosens them. This regime is not theoretical — it was calibrated after a real ban incident: night sending plus a sudden volume spike plus rapid repeats to one recipient earned the account a 7.5-hour temporary block. Since then the rule is absolute: the values are constants in anti-ban.js, never moved to the database or UI. The temptation of "we're in a hurry today, drop the delay to 10 seconds" is architecturally locked out.
Why is randomness essential?
Humans don't message like metronomes. Before every message a random value between 45 and 90 seconds is drawn; no two gaps are identical. Burst pauses are likewise randomly chosen between 8 and 12 minutes. The resulting traffic pattern is statistically similar to a person typing messages one by one.
The hour window and timezones
The window is computed from the container's local time, which makes the TZ environment variable critical. A wrong timezone silently shifts the window and can cause 3 a.m. sends. Jobs outside the window are not cancelled — the worker computes the time until the window opens and delays accordingly, resuming at 05:00 sharp.
Planning example: A 300-recipient announcement at 25–30 messages/hour takes roughly 10–12 hours including burst pauses. Start at 08:00 and it finishes in the evening. Bulk WhatsApp is the wrong channel for "urgent" blasts that can't be spread over time — senders who accept this up front keep their numbers healthy for years.
7. How scheduled sending works
When you pick a date and time on the Compose screen, this is what happens:
- The server writes each recipient's message to the database with a
scheduled_attimestamp. - The job is added to BullMQ with a
delaycomputed to the target time, and sleeps in Redis. - When the time arrives the job wakes and enters the normal anti-ban flow: hour window first, then recipient cooldown, then random delay. "Scheduled for 9 p.m." means "delivered starting at 9 p.m., at a safe pace, in order" — not "at exactly 9 p.m."
- Once sent, the record transitions to
sentand the WhatsApp message ID is stored.
Scheduled messages appear under Messages → Scheduled. Cancellation is one click and does two things: soft-deletes the database record (status cancelled) and removes the pending job from Redis. A message already sent naturally cannot be cancelled.
8. Sending media (images, videos, documents)
Beyond text, the system sends images, videos and documents — but media is a far more "suspicious" signal to WhatsApp's spam classifier. For dispatches containing media, the tempo automatically halves: delays grow 1.5× (67–135 s) and burst pauses double (16–24 min). Additionally, after a real incident where large video uploads timed out on slow networks and caused duplicate sends, the send timeout was set to 120 seconds.
Practical tips: keep images under 1 MB, share videos as links where possible, and prefer text-plus-one-image over heavy attachments for campaigns.
9. Use cases and ready-to-use message templates
These templates are distilled from real scenarios; fill the bracketed fields with your own data. They share four traits: short, personal, single-purpose, with an opt-out line where required.
9.1 Appointment reminder (transactional)
9.2 Order / shipping notification (transactional)
9.3 Donation receipt / thank-you (nonprofit)
9.4 Event announcement (campaign — consent required)
9.5 Dues / payment reminder
9.6 Promotion (consent + opt-out line mandatory)
One template rule never changes: personalize at least the name field instead of sending character-for-character identical text to hundreds of recipients. Identical content produces a spam signal via content fingerprinting; even a [Name] field breaks that signature.
10. Content and timing strategy
10.1 Content writing rules
- The first sentence is everything: only the first line shows in the notification preview. State your purpose in the first 8–10 words.
- One message, one purpose: don't combine a promotion, an event and a survey. Both readership and click-through drop.
- Keep it short: 300–500 characters is the sweet spot. Move longer content behind a link.
- Use links with discipline: don't put a link in every message; avoid URL shorteners (bit.ly etc.) — unknown short links breed distrust in both recipients and spam filters. Use your own domain.
- Emoji in moderation: 1–2 add warmth; emoji on every line erodes credibility.
- Identify yourself: the recipient may not have saved your number; sign the message with who you are.
10.2 Timing tactics
- Best hours: weekdays 10:00–12:00 and 14:00–18:00. Before 8 a.m. and after 9 p.m., open rates fall and complaint rates rise.
- Avoid Monday morning and Sundays: you either drown in message noise or intrude on weekend rest.
- Spread large lists over time: start 300+ recipient dispatches in the morning so they finish inside the window (see the planning example).
- Frequency: more than 1–2 campaign messages per week to the same audience raises complaint rates fast. Transactional notices (shipping, appointments) don't count against this.
- Use scheduling: drafting an announcement at night and scheduling it for 10:00 respects the window rule and lands at the most effective hour.
11. Step-by-step installation
Requirements: a machine with Docker + Docker Compose (2 GB RAM recommended — Chromium will be running), a free Supabase project, and a phone with WhatsApp installed. Total time: ~20 minutes for a first install.
Step 1 — Prepare the database
Open your Supabase project's SQL Editor and run the contents of supabase/migrations/001_init.sql from the repository. This creates the message and session tables, indexes and status enums.
Step 2 — Fill in the environment variables
cp .env.example .env
Then edit .env. Generate long, random values for REDIS_PASSWORD and WA_CONTROL_SECRET:
openssl rand -hex 32 # run separately for each secret
Step 3 — Start the system
docker compose up -d --build
The first build takes a few minutes (Chromium dependencies are downloaded). Four services come up: web, server, redis, whatsapp-worker.
First boot needs internet access: the worker downloads the current WhatsApp Web version HTML from GitHub. A fully air-gapped first start will fail.
Step 4 — Pair your phone via QR
- Open
http://localhost:8080in your browser. - Go to the Connection page and click Start Session.
- On your phone: WhatsApp → Settings → Linked Devices → Link a Device, then scan the QR code.
- Once the status reads
connected, the system is ready to send.
Because the session is written to a persistent volume via LocalAuth, you only ever do this once; container restarts do not break the session.
12. Environment variable reference
| Variable | Required | Description |
|---|---|---|
TZ | Yes | The basis of the hour-window computation, e.g. Europe/Istanbul. If wrong, the window shifts and you start sending at night. |
REDIS_PASSWORD | Yes | Redis password. Long and random. |
SUPABASE_URL | Yes | Your Supabase project URL. |
SUPABASE_SERVICE_ROLE_KEY | Yes | Service role key (bypasses RLS). Server-side only — never expose it to the browser. |
WA_CONTROL_SECRET | Yes | Shared secret for the worker control API. If missing, the worker refuses to start (fail-fast). |
WEB_PORT | No | Externally exposed panel port (default 8080). |
WA_MEDIA_REWRITE_HOST | No | Rewrites the media URL host in development environments. |
13. Production: deploying with Dokploy
The system runs on any platform that supports Docker Compose; in production it has been tested with Dokploy. Seven rules are vital:
- Protect the session volume. The
whatsapp-sessionsvolume must survive across deploys. Losing it doesn't lose data, but you'll re-scan the QR. - Don't disable Redis persistence. AOF is enabled by the compose file; scheduled jobs live in Redis. With persistence off, every deploy wipes all scheduled messages.
- Never expose port 3100. The worker control API must be reachable only from the internal Docker network. Bind your domain to the web service (8080) only.
- Never scale the worker horizontally. Two Chromium instances opening the same LocalAuth folder corrupts the session.
- Use a stop-then-start deploy strategy. The new container must not touch
/sessionsuntil the old one has fully shut down (90-second grace period). Rolling updates cause a session race. - Verify
TZ. Ifdateinside the container doesn't show your local time, the hour window is on the wrong clock. - Ensure internet access on first boot. The worker fetches the WhatsApp Web version file from GitHub.
14. Golden operational rules
- Don't send at night — you can't. There is deliberately no option to disable the hour window. The system waits automatically after 23:00.
- Warm up new numbers slowly. Don't send hundreds of messages on day one with a freshly connected number. Start at 20–50 messages/day for the first week and ramp up; WhatsApp is especially sensitive to new-account-plus-sudden-volume. If possible, connect a number with weeks of genuine two-way chat history.
- "Disconnect" is not a logout. The panel's disconnect only stops the client; the device pairing on your phone is preserved. To fully unlink, use Linked Devices on the phone.
- If you get blocked, stop. On a temporary block, set the session to
bannedand halt sending entirely. Forcing sends during a block is the surest way to convert it into a permanent one. - Keep your lists clean. Don't persist with recipients who blocked you or never respond; complaint rate is the heaviest ban signal. Remove every STOP the same day.
- Use the number both ways. Numbers that only send and never receive look suspicious. Keep using the connected number for normal conversations too.
15. Measurement and reporting
Every message follows a lifecycle in the database; the panel surfaces it at three levels:
| Status | Meaning | Where to see it |
|---|---|---|
queued | Enqueued, waiting for its turn/window | Messages → Pending |
scheduled | Future-dated; will enter the queue when due | Messages → Scheduled |
sent | Delivered to WhatsApp, message ID stored | Messages → Sent + Dashboard |
failed | Could not be sent; error reason stored | Messages → Failed |
cancelled | Cancelled by the user | Messages → Cancelled |
Three metrics worth tracking: failure rate (consistently above 5% means poor list quality — purge invalid numbers), reply rate (the real impact of your campaigns, observed on the phone) and opt-out/block feedback (rising means your content or frequency is off). Because all data lives in your own Supabase project, you can derive any report with SQL or connect BI tools — you're not confined to a third-party panel's data limits.
16. Troubleshooting
The QR code doesn't appear
On first boot the worker downloads the WhatsApp Web version file; on slow connections QR generation can take 30–60 seconds. Still nothing? Check docker compose logs whatsapp-worker — is there internet access, and is WA_CONTROL_SECRET set?
Status stuck on "connecting"
Most common cause: two worker replicas opening the same session folder (see the single-replica rule), or a stale Chromium lock. Stop the worker, confirm exactly one replica, restart.
Messages sit in "queued" and never send
Three causes, in order: (1) The hour window — between 23:00 and 05:00 the system is deliberately waiting. (2) Wrong TZ — verify with date inside the container. (3) Session down — check the Connection page.
The session dropped on its own
Check Linked Devices on the phone; if the pairing is gone, re-scan the QR. If the phone stays offline ~14 days, WhatsApp drops linked devices. Also verify the session volume wasn't wiped during a deploy.
The same message was sent twice
Historical cause: timeouts on large media uploads. The current send timeout is 120 seconds and this incident class is closed. If you still see it, two worker replicas may be running — drop to one.
WhatsApp applied a temporary block
Stop sending entirely, mark the session banned, and wait out the block. Afterwards, re-warm with low volume (20–30/day) for the first days. Ask what caused it: list quality, content repetition, or a volume spike?
17. API reference: using the system programmatically
The panel does all of its work through this HTTP API, and your own application can call the same endpoints:
| Endpoint | Method | Function |
|---|---|---|
/api/dispatch | POST | Creates a bulk or scheduled dispatch (recipients, text, media, optional date). |
/api/messages | GET | Lists messages with status filters. |
/api/messages/:id/cancel | POST | Cancels a pending/scheduled message. |
/api/dispatches | GET | Lists dispatch groups. |
/api/stats | GET | Daily sending statistics. |
/api/session/status | GET | WhatsApp session status. |
/api/session/qr | GET | Current QR code for pairing. |
/api/session/start | POST | Starts the session. |
/api/session/disconnect | POST | Stops the client (phone pairing preserved). |
/api/health | GET | Health check. |
Phone numbers are normalized with a Türkiye-first policy: 05xx…, 5xx… and 905xx… formats are converted to international form automatically; invalid numbers are filtered before sending and marked failed.
18. Frequently asked questions
Is sending bulk messages on WhatsApp legal?
Yes, if the recipient has given explicit consent. Commercial messages fall under data-protection law (GDPR and national equivalents); you must obtain prior opt-in and offer an opt-out in every message. Transactional notices (shipping, appointments) are generally treated differently. See the legal framework section.
Does this system use the official WhatsApp Business API?
No. It pairs the WhatsApp account on your phone via QR code (whatsapp-web.js). No Business API application, no template approval, no per-message fee.
How is this different from WhatsApp's built-in broadcast lists?
Broadcast lists are capped at 256 recipients and only reach people who saved your number. This system has no contact requirement and no 256 limit; messages arrive as normal one-to-one chats, can be scheduled, and are tracked from the panel.
How does it protect my number from getting banned?
The anti-ban regime is hard-coded: random 45–90 s delays, an 8–12 min pause every 15 messages, sending only 05:00–23:00, a 4-hour per-recipient cooldown, and ~25–30 messages/hour. Deliberately not user-configurable — nobody can endanger the number by "speeding things up."
How many messages can I send per day?
~25–30 messages/hour × an 18-hour window = a theoretical ceiling of 450–500/day. In practice, start new numbers at 20–50/day for the first week and ramp up.
Will recipients know the message was sent in bulk?
No. Each message arrives as a normal one-to-one chat; there is no bulk label. With name personalization the experience is completely natural.
Can I schedule messages for a future date and time?
Yes. Picking a date and time on Compose creates the job with BullMQ's delay feature. When due, it is sent through the normal anti-ban flow, and can be cancelled with one click from Messages → Scheduled.
Can the system send while my phone is off?
Yes. WhatsApp's multi-device architecture lets linked devices work independently of the phone. The phone must come online roughly every 14 days to keep the account active.
What do I need to install it, and what does it cost?
A Docker server (~$5–10/month), a free Supabase project, and a phone with WhatsApp. The software is open source; there are no per-message fees. Setup is three steps: run the SQL migration, fill in .env, run docker compose up -d --build.
Can I send images, videos or documents?
Yes. Because media is riskier than text, the system slows down automatically: delays grow to 67–135 s and burst pauses extend to 16–24 min.
Do I lose my session or queue if the server restarts?
No. The session persists via LocalAuth in a durable volume — no QR re-scan. Redis AOF persistence carries scheduled jobs across restarts.
What happens if a message fails to send?
The record transitions to failed with the error reason and appears on the Messages page. Invalid numbers are normalized and filtered before sending.
Can I connect more than one WhatsApp number?
The system is designed around a single worker and a single session. Two Chromium instances opening the same session folder corrupts it, so the worker is never horizontally scaled.
Is it safe to send spam with this system?
No, and that is not its purpose. It is designed for human-speed notifications to your own opted-in audience. Unsolicited bulk messaging violates WhatsApp's Terms of Service and data-protection law, and will get your number permanently banned.
Conclusion
There are four ways to send bulk WhatsApp messages; the right choice depends on your volume, budget and need for content freedom. For legitimate traffic of a few hundred notifications a day, the QR-based system in this guide offers the most practical balance: a notification pipeline managed from a React panel, queued through BullMQ, delivered at human speed by a single disciplined worker, with a complete audit trail in Supabase. Setup is three commands, the session is persistent, the data stays entirely yours — and the anti-ban regime knows how to say "no" on your behalf. Stick to consent + opt-out + clean lists on the legal side, and warm-up + window + tempo discipline on the operational side, and this system will run for years without wearing out your number.
All Guides
Deep-dive guides on every part of WhatsApp bulk messaging — pick a topic: