Je eigen WhatsApp Bot draaien op een VPS: Complete Handleiding

· 21 min leestijd

Voor ~€5/maand en een prepaid SIM heb je een WhatsApp bot die berichten stuurt naar je groepschat, webhooks ontvangt, en dagelijkse overzichten verstuurt. Volledig in eigen beheer, gehost in Nederland.

Je eigen WhatsApp Bot draaien op een VPS: Complete Handleiding

TL;DR Voor ~€5/maand en een prepaid SIM heb je een WhatsApp bot die berichten stuurt naar je groepschat, webhooks ontvangt, en dagelijkse overzichten verstuurt. Volledig in eigen beheer, gehost in Nederland. In deze post leg ik stap voor stap uit hoe je dit opzet.

Inhoudsopgave

  1. Waarom een WhatsApp bot?
  2. Waarom zelf hosten, en niet via een platform?
  3. Wat heb je nodig?
  4. Stap 1: VPS bestellen
  5. Stap 2: SIM-kaart regelen
  6. Stap 3: VPS inrichten
  7. Stap 4: WhatsApp bot bouwen
  8. Stap 5: WhatsApp koppelen
  9. Stap 6: Webhook server toevoegen
  10. Stap 7: Groep-whitelist & veiligheid
  11. Stap 8: Dagelijks overzicht schedulen
  12. Stap 9: Bot 24/7 draaien met PM2
  13. Stap 10: Monitoring & onderhoud
  14. Verder uitbouwen: ideeën voor features

⚠️ Disclaimer

Deze handleiding maakt gebruik van whatsapp-web.js, een open-source library die WhatsApp Web simuleert. Dit is niet officieel ondersteund door Meta en is technisch gezien in strijd met de WhatsApp gebruiksvoorwaarden. Het bot-account kan door WhatsApp geblokkeerd worden, met name bij intensief of commercieel gebruik. Voor intern gebruik binnen een klein team is het risico in de praktijk gering, maar gebruik is op eigen risico.

De beschikbaarheid van deze oplossing is afhankelijk van de open-source maintainers van whatsapp-web.js. Als Meta iets aanpast aan WhatsApp Web, kan de library tijdelijk niet werken totdat er een update uitkomt.

Verstuur je via de bot berichten met persoonsgegevens (namen, boekingsinfo, contactgegevens)? Dan val je onder de AVG/GDPR en ben je als verwerkende partij zelf verantwoordelijk voor een correcte omgang met die data.

De auteur is niet aansprakelijk voor geblokkeerde accounts, dataverlies of andere gevolgen van het gebruik van deze handleiding.

Waarom een WhatsApp bot?

Laten we eerlijk zijn: e-mail notificaties worden nauwelijks gelezen. Iedereen heeft zijn inbox ingesteld op "alleen lezen als ik er zin in heb." Slack is in theorie handig, maar voor veel teams is het weer een extra app die je moet openen, instellen en beheren. En voor klanten of kleinere teams is het al helemaal een drempel.

Maar WhatsApp? Dat checkt iedereen. Meerdere keren per dag. Automatisch. WhatsApp heeft in Nederland een gebruikersdichtheid van boven de 90%. Het is de meest gebruikte communicatietool van het land, van de bakker om de hoek tot de grotere bedrijven.

Wij gebruiken een WhatsApp bot als een soort virtuele assistent die het team op de hoogte houdt van wat er speelt:

  • 📋 Dagoverzicht: elke ochtend automatisch een overzicht van de boekingen of taken van die dag, klaar als iedereen op kantoor of in de zaak arriveert
  • 🔔 Real-time meldingen: direct een berichtje in de groep wanneer een nieuwe boeking, bestelling of melding binnenkomt, zonder dat iemand een dashboard in de gaten hoeft te houden
  • 🤖 Commando's: mention de bot in de groepschat om snel een overzicht op te vragen, zonder systemen te hoeven openen

Het mooiste? Het kost bijna niks om te draaien. Geen dure SaaS-abonnementen, geen per-bericht-kosten, geen vendor lock-in. Gewoon een server van vijf euro per maand en een prepaid SIM-kaartje.

Waarom zelf hosten, en niet via een platform?

Dit is een vraag die je jezelf mag stellen voordat je begint. Er bestaan commerciële platformen waarmee je WhatsApp-berichten kunt sturen: Twilio, MessageBird (nu Bird), 360dialog en anderen. Ze werken via de officiële WhatsApp Business API en zijn voor grote volumes of enterprise-toepassingen uitstekend.

Maar voor de meeste kleine en middelgrote toepassingen zijn ze onnodig duur, ingewikkeld, en je bent afhankelijk van een derde partij voor iets wat eigenlijk heel simpel is. Laten we die afweging eerlijk maken.

Commercieel platform vs. zelf hosten

Commercieel platform Zelf hosten (deze handleiding)
Kosten€0,005 tot €0,10 per bericht + maandelijkse licentie~€5/maand vaste kosten
SetupRelatief eenvoudigWat technische kennis nodig
AfhankelijkheidVolledig van de providerAlleen van jezelf
PrivacyBerichten lopen via servers van derdenAlles op je eigen server
AanpasbaarheidBeperkt tot API-mogelijkhedenVolledig vrij
StabiliteitGebonden aan provider-updates en -beleidJij bepaalt wanneer je update
WhatsApp ToSOfficieel ondersteundGebruik op eigen risico*

*Een eerlijke kanttekening: whatsapp-web.js simuleert WhatsApp Web via een headless browser. WhatsApp staat dit technisch gezien niet expliciet toe in hun gebruiksvoorwaarden. Voor persoonlijk of intern zakelijk gebruik werkt het prima en is het risico minimaal. Voor klantgerichte toepassingen misschien toch beter om de officiële Business API te gebruiken.

Privacy en datasoevereiniteit

Dit is voor veel bedrijven een steeds belangrijker punt. Wanneer je berichten via een commercieel platform stuurt, worden die berichten verwerkt op servers die buiten jouw controle liggen, vaak in de VS of andere landen buiten de EU.

Als je zelf host op een Nederlandse VPS:

  • Je berichten verlaten jouw eigen infrastructuur niet voor verwerking
  • Alle data valt onder Nederlandse en Europese wetgeving (AVG/GDPR)
  • Je hebt geen verwerkersovereenkomst nodig met een externe partij voor de berichtverwerking zelf
  • Er is geen risico dat een provider plots zijn prijzen verhoogt, zijn beleid wijzigt, of zijn deuren sluit

Voor TransIP geldt dat de servers fysiek in Amsterdam staan. Dat betekent lage latency voor Nederlandse gebruikers én een stevige juridische positie als het gaat om databescherming.

Geen afhankelijkheid van Meta's officiële kanalen

De officiële WhatsApp Business API vereist goedkeuring door Meta, een geregistreerd bedrijfsnummer, en een review-proces dat weken kan duren. Bovendien betaal je per gesprek of per bericht. Voor een intern team-notificatiesysteem is dat overkill.

Met whatsapp-web.js heb je dit allemaal niet nodig. Je logt in met een gewoon WhatsApp-account (op een aparte SIM), koppelt dat via een QR-code, en je bent klaar. In een middag.

Wat heb je nodig?

Onderdeel Kosten Opmerking
VPS~€5/maandKlein is genoeg
SIM-kaartEenmalig ~€5Prepaid of goedkope sim-only
Oud telefoon-toestelEenmaligVoor de eerste koppeling
Domeinnaam (optioneel)~€10/jaarHandig voor SSL + webhooks

💰 Kostenplaatje per maand

VPS (TransIP V1) .............. €5,00

SIM (prepaid, eenmalig) ....... €0,42 ← €5 / 12 maanden

Domein (optioneel) ............ €0,83 ← €10 / 12 maanden

Totaal ........................ €5,42 ← minder dan een kop koffie ☕

Je hebt ook basiskennis van Linux en de terminal nodig. Je hoeft geen expert te zijn. Alle commando's staan hieronder letterlijk uitgeschreven, maar SSH weten te gebruiken en niet schrikken van een command prompt is handig.

Stap 1: VPS bestellen

Je hebt een server nodig die 24/7 draait. Een goedkope VPS is meer dan genoeg. De bot gebruikt minimaal resources. Denk aan minder dan 200MB RAM en verwaarloosbaar CPU-gebruik in rust.

Aanrader: TransIP

👉 TransIP VPS pakketten

  • VPS V1: €5/maand
  • OS: Ubuntu 24.04 LTS (of nieuwer)
  • Gehost in Amsterdam 🇳🇱, lekker lage latency, én je data blijft in Nederland

TransIP is een Nederlandse partij, opgericht in Leiden, en al jaren de go-to provider voor Nederlandse developers en bedrijven die waarde hechten aan lokale hosting. Hun support is Nederlandstalig en hun infrastructuur is solide.

💡 Tip:

Kies altijd voor een Ubuntu LTS versie. Die krijgt 5 jaar security updates, dus je hoeft niet steeds je OS te upgraden. Ubuntu 24.04 LTS is een goede keuze en draait uitstekend op een kleine VPS.

Er zijn uiteraard alternatieven: Hetzner (Duits, ook uitstekend voor privacy), DigitalOcean, Vultr. Allemaal prima. Hetzner heeft zelfs servers in Nuremberg en Helsinki, beide binnen de EU. Als het maar Linux draait en je er via SSH op kunt.

Waarom geen Nederlandse hosting bij een andere partij? Dat kan uiteraard ook. Antagonist, Yourhosting, Hostnet. Allemaal betrouwbare Nederlandse partijen. Check wel of je root-toegang krijgt en of Node.js draait op het gekozen pakket. Een managed webhosting-pakket is hiervoor niet geschikt; je hebt echt een VPS nodig.

Stap 2: SIM-kaart regelen

De bot heeft een eigen WhatsApp-account nodig. Gebruik hiervoor nooit je persoonlijke nummer. Koop een aparte SIM. Dit hoeft echt niet veel te kosten.

Optie A: KPN Prepaid, eenmalig €5

👉 KPN Prepaid

  • Goedkoopste optie
  • ⚠️ Let op: Hou je kaart actief! KPN deactiveert prepaid SIMs die te lang niet gebruikt worden. Stel een herinnering in om elke paar maanden een SMS'je te sturen of een belletje te plegen.

Optie B: Ben Sim-Only vanaf ~€9/maand

👉 Ben Sim-Only

  • Geen gedoe met actief houden
  • Iets duurder, maar zorgeloos

Setup van de SIM

  1. Activeer de SIM in een oud (of geleend) toestel
  2. Installeer WhatsApp en registreer het nummer
  3. Voeg het nummer toe aan je WhatsApp groepschat waar de bot berichten naartoe moet sturen

🔒 Privacy tip:

Dit nummer wordt nooit publiek gedeeld. Het is puur voor de bot. Geef het een herkenbare naam in WhatsApp, zoals "Team Bot" of "Notificatie Bot", zodat iedereen in de groep weet waar de berichten vandaan komen.

Stap 3: VPS inrichten

SSH naar je nieuwe VPS en installeer de benodigde software. Als dit je eerste keer is met SSH: je logt in via ssh root@jouw-server-ip en voert het wachtwoord in dat je van TransIP hebt gekregen.

# System updaten
sudo apt update && sudo apt upgrade -y

# Node.js 20 LTS installeren (via NodeSource)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Chromium installeren (nodig voor whatsapp-web.js)
sudo apt install -y chromium-browser

# Controleer installatie
node --version    # v20.x.x
npm --version     # 10.x.x
chromium-browser --version

Waarom Chromium?

De library whatsapp-web.js gebruikt Puppeteer om een Chromium browser headless (dus zonder scherm) te draaien. Zo simuleert het precies hoe WhatsApp Web in een gewone browser werkt, maar dan volledig geautomatiseerd op je server, zonder dat er een monitor of muis aan te pas komt.

Dit is ook meteen de reden waarom de bot behoorlijk betrouwbaar is: het is geen reverse-engineerde API, maar een echte WhatsApp Web-sessie die gewoon draait op je server.

Stap 4: WhatsApp bot bouwen

Project aanmaken

mkdir ~/whatsapp-bot && cd ~/whatsapp-bot
npm init -y

Dependencies installeren

npm install whatsapp-web.js qrcode-terminal express node-cron
Package Waarvoor
whatsapp-web.jsWhatsApp Web client via Puppeteer
qrcode-terminalQR code tonen in terminal voor eerste login
expressWebhook server voor inkomende berichten
node-cronScheduled taken (bijv. dagelijks overzicht)

Vier packages. Geen enorme dependency-boom, geen framework dat je moet leren kennen. Dit is expres zo gehouden: hoe minder afhankelijkheden, hoe minder er kapot kan gaan bij updates.

Bot code schrijven

Maak een index.js aan:

const { Client, LocalAuth } = require("whatsapp-web.js");
const qrcode = require("qrcode-terminal");
const express = require("express");
const cron = require("node-cron");

// ─── Configuratie ───────────────────────────────────────────────────

const CHAT_ID = process.env.CHAT_ID || "";
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || "";
const PORT = parseInt(process.env.PORT || "3000", 10);

if (!CHAT_ID) {
  console.warn("⚠  CHAT_ID niet ingesteld — bot toont beschikbare chats bij opstarten");
}
if (!WEBHOOK_SECRET) {
  console.error("❌ WEBHOOK_SECRET is verplicht voor beveiliging");
  process.exit(1);
}

// ─── WhatsApp Client ─────────────────────────────────────────────────

const client = new Client({
  authStrategy: new LocalAuth(),
  puppeteer: {
    args: [
      "--no-sandbox",
      "--disable-setuid-sandbox",
      "--disable-gpu",
      "--disable-dev-shm-usage",
      "--single-process",
    ],
    ...(process.env.CHROMIUM_PATH
      ? { executablePath: process.env.CHROMIUM_PATH }
      : {}),
    protocolTimeout: 120000,
  },
});

let isReady = false;
let groupChatValidated = false;

client.on("qr", (qr) => {
  console.log("\\n📱 Scan deze QR code met WhatsApp:\\n");
  qrcode.generate(qr, { small: true });
});

client.on("ready", async () => {
  isReady = true;
  console.log("✅ WhatsApp client gereed");

  const chats = await client.getChats();
  const groups = chats.filter((c) => c.isGroup);

  console.log("\\n📋 Beschikbare groepschats:");
  groups.forEach((g) => {
    console.log(\`  → "\${g.name}" — ID: \${g.id._serialized}\`);
  });

  if (!CHAT_ID) {
    console.warn("\\n⚠  Stel CHAT_ID in op een van de bovenstaande groep-IDs.\\n");
    return;
  }

  try {
    const targetChat = await client.getChatById(CHAT_ID);
    if (targetChat.isGroup) {
      groupChatValidated = true;
      console.log(\`✅ CHAT_ID bevestigd als groepschat: "\${targetChat.name}"\`);
    } else {
      groupChatValidated = false;
      console.error(\`\\n🚨 CHAT_ID is GEEN groepschat — het is een DM!\`);
    }
  } catch (err) {
    groupChatValidated = false;
    console.error(\`\\n🚨 CHAT_ID niet gevonden — controleer je .env bestand.\\n\`);
  }
});

client.on("disconnected", (reason) => {
  isReady = false;
  groupChatValidated = false;
  console.error("❌ WhatsApp verbinding verbroken:", reason);
  setTimeout(() => {
    console.log("🔄 Opnieuw verbinden...");
    client.initialize();
  }, 10_000);
});

client.on("auth_failure", (msg) => {
  console.error("❌ Authenticatie mislukt:", msg);
});

console.log("🔄 WhatsApp client starten...");
client.initialize();

Environment variabelen instellen

Maak een .env bestand aan. Hierin sla je configuratie op die je nooit in je code wilt hardcoderen, zeker geen secrets:

# .env

# WhatsApp groep ID (vul je in na eerste run)
CHAT_ID=

# Gedeeld geheim voor webhook-beveiliging
# Genereer met: openssl rand -hex 32
WEBHOOK_SECRET=jouw-geheim-hier

# Express server poort
PORT=3000

# Pad naar Chromium op je VPS
CHROMIUM_PATH=/usr/bin/chromium-browser

💡 Tip:

Genereer een sterk webhook secret:

openssl rand -hex 32

En een .gitignore zodat je geen gevoelige bestanden per ongeluk naar GitHub pusht:

node_modules/
.env
.wwebjs_auth/
.wwebjs_cache/

Die .wwebjs_auth/ map is belangrijk: daarin slaat whatsapp-web.js je WhatsApp-sessie op. Als je die per ongeluk deelt, kan iemand anders inloggen op je bot-account. Hou die map dus altijd privé.

Stap 5: WhatsApp koppelen

Tijd voor het magische moment. Zorg dat je de telefoon met de bot-SIM bij de hand hebt:

cd ~/whatsapp-bot
node index.js

Het koppelingsproces:

  1. Er verschijnt een QR code in je terminal
  2. Open WhatsApp op de telefoon met je bot-SIM
  3. Ga naar Instellingen → Gekoppelde apparaten → Apparaat koppelen
  4. Scan de QR code

De bot logt nu in en toont alle beschikbare groepschats:

✅ WhatsApp client gereed

📋 Beschikbare groepschats:
  → "Team Chat" — ID: 120363XXXXXXXXX@g.us
  → "Andere Groep" — ID: 120363YYYYYYYYY@g.us

Kopieer de juiste groeps-ID naar je .env:

CHAT_ID=120363XXXXXXXXX@g.us

Herstart de bot en je ziet:

✅ CHAT_ID bevestigd als groepschat: "Team Chat"

🎉 Je bot is gekoppeld!

Na de eerste koppeling hoef je de telefoon nooit meer te gebruiken. De sessie wordt opgeslagen in .wwebjs_auth/ en bij een herstart pikt de bot automatisch de sessie op. Je telefoon kan dus in een la liggen, of je geeft hem weg en houdt alleen de SIM over.

Stap 6: Webhook server toevoegen

Nu willen we dat externe systemen (je eigen backend, een boekingssysteem, een webshop, wat dan ook) berichten kunnen sturen via de bot. Dit werkt via een eenvoudige HTTP webhook.

Voeg dit toe aan je index.js:

// ─── Veilig berichten sturen ─────────────────────────────────────────

async function sendToGroup(message) {
  if (!isReady || !CHAT_ID) {
    console.warn("⏭  Kan niet versturen — WhatsApp niet gereed");
    return false;
  }
  if (!groupChatValidated) {
    console.error("❌ CHAT_ID is niet gevalideerd als groepschat");
    return false;
  }
  try {
    await client.sendMessage(CHAT_ID, message);
    return true;
  } catch (err) {
    console.error("❌ Bericht versturen mislukt:", err);
    return false;
  }
}

// ─── Express Server ──────────────────────────────────────────────────

const app = express();
app.use(express.json());

// Health check endpoint
app.get("/health", (_, res) => {
  res.json({
    status: "ok",
    whatsapp: isReady ? "connected" : "disconnected",
    chatId: CHAT_ID ? "configured" : "not set",
    groupValidated: groupChatValidated,
    uptime: Math.floor(process.uptime()),
  });
});

// Webhook endpoint — ontvangt berichten van je backend
app.post("/webhook", async (req, res) => {
  const secret = req.headers["x-webhook-secret"];
  if (secret !== WEBHOOK_SECRET) {
    console.warn("⚠  Ongeautoriseerde webhook poging");
    return res.sendStatus(401);
  }

  const { message } = req.body;
  if (!message || typeof message !== "string") {
    return res.status(400).json({ error: "Geen bericht meegegeven" });
  }

  try {
    const sent = await sendToGroup(message);
    if (sent) {
      console.log("✅ Bericht verstuurd naar groep");
      res.json({ ok: true });
    } else {
      res.status(503).json({ error: "Groepschat niet gereed" });
    }
  } catch (err) {
    console.error("❌ Bericht versturen mislukt:", err);
    res.status(500).json({ error: "Versturen mislukt" });
  }
});

app.listen(PORT, () => {
  console.log(\`🚀 Webhook server draait op poort \${PORT}\`);
});

Webhook aanroepen vanuit je backend

Nu kun je vanuit elk backend-systeem een bericht sturen. Een simpele curl-aanroep is alles wat je nodig hebt:

curl -X POST https://jouw-server.nl/webhook \\
  -H "Content-Type: application/json" \\
  -H "X-Webhook-Secret: jouw-geheim-hier" \\
  -d '{"message": "🔔 Nieuwe bestelling binnengekomen!"}'

Vanuit PHP (bijv. Laravel of WordPress):

$response = Http::withHeaders([
    'X-Webhook-Secret' => env('WHATSAPP_WEBHOOK_SECRET'),
])->post('https://jouw-server.nl/webhook', [
    'message' => "🔔 Nieuwe boeking: {$booking->name} om {$booking->time}",
]);

Vanuit Python:

import requests

requests.post(
    "https://jouw-server.nl/webhook",
    headers={"X-Webhook-Secret": "jouw-geheim-hier"},
    json={"message": "🔔 Nieuwe melding binnengekomen!"}
)

De bot is nu een universele notificatie-laag die je aan elk systeem kunt hangen.

🔒 Belangrijk:

Stel je VPS firewall in zodat poort 3000 niet publiek open staat. Gebruik een reverse proxy (Nginx of Caddy) met SSL, of beperk toegang tot specifieke IP-adressen. Hierover meer in de beveiligingsstap.

Stap 7: Groep-whitelist & veiligheid

De bot mag alleen berichten sturen naar de geverifieerde groepschat. Hier zijn de beveiligingslagen die we hebben ingebouwd, en waarom ze elk belangrijk zijn.

1. Groep-validatie bij opstarten

De bot checkt bij het opstarten of de CHAT_ID daadwerkelijk een groepschat is. Als het een persoonlijk gesprek (DM) is, weigert de bot berichten te sturen. Dit voorkomt dat je per ongeluk een privégesprek van een teamlid volspamt.

2. Alleen reageren in de eigen groep

Als je de bot ook op commando's wilt laten reageren (bijv. als iemand de bot mention), limiteer je dat tot de eigen groepschat:

client.on("message", async (msg) => {
  const chat = await msg.getChat();

  // Negeer alle DMs
  if (!chat.isGroup) return;

  // Alleen reageren in de gewhiteliste groep
  if (chat.id._serialized !== CHAT_ID) return;

  // Alleen reageren als de bot gementioned wordt
  const mentions = await msg.getMentions();
  const botId = client.info?.wid?._serialized;
  const isMentioned = mentions.some((m) => m.id._serialized === botId);
  if (!isMentioned) return;

  // Verwerk het commando
  const body = msg.body.toLowerCase().replace(/@\\d+/g, "").trim();
  console.log(\`📩 Commando ontvangen: "\${body}"\`);

  if (body.includes("status")) {
    await chat.sendMessage("✅ Bot is online en actief!");
    return;
  }

  await chat.sendMessage(
    "🤖 *Bot Commando's*\\n\\n" +
    "Mention mij met een van deze commando's:\\n\\n" +
    "📊 *status* — Controleer of de bot actief is"
  );
});

3. Webhook authenticatie

Elke webhook-request moet een X-Webhook-Secret header meesturen. Zonder het juiste secret → 401 Unauthorized. Behandel dit secret als een wachtwoord: genereer het willekeurig, sla het alleen op in je .env, en gebruik het nooit in client-side code.

4. Nginx reverse proxy instellen

In plaats van poort 3000 direct bloot te stellen, zet je Nginx als tussenpersoon. Dit geeft je SSL/HTTPS en een propere URL:

sudo apt install -y nginx certbot python3-certbot-nginx

Maak een Nginx config aan:

server {
    listen 80;
    server_name jouw-server.nl;

    location /webhook {
        proxy_pass http://localhost:3000/webhook;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location /health {
        proxy_pass http://localhost:3000/health;
    }
}

SSL-certificaat aanvragen (gratis via Let's Encrypt):

sudo certbot --nginx -d jouw-server.nl

Samenvatting beveiligingslagen

Laag Bescherming
Groep-validatieVoorkomt dat berichten naar DMs gaan
Chat-whitelistBot reageert alleen in de juiste groep
Mention-onlyBot reageert alleen als hij expliciet gementioned wordt
Webhook secretAlleen geautoriseerde systemen kunnen berichten triggeren
Nginx + SSLBeschermt de webhook server met HTTPS
FirewallPoort 3000 is niet direct publiek bereikbaar

Stap 8: Dagelijks overzicht schedulen

Met node-cron kun je de bot elke dag op een vast tijdstip een bericht laten sturen. Bijvoorbeeld een dagelijks overzicht van taken, afspraken, of orders. Cron-expressies volgen het patroon minuut uur dag maand weekdag.

const cron = require("node-cron");

// Elke dag om 10:00 uur (Amsterdam tijdzone)
cron.schedule("0 10 * * *", () => {
  console.log("⏰ Dagelijks overzicht starten...");
  sendDailyOverview();
}, { timezone: "Europe/Amsterdam" });

async function sendDailyOverview() {
  try {
    const items = await fetchTodaysItems();

    if (items.length === 0) {
      await sendToGroup("📋 Geen items voor vandaag 🏖️");
      return;
    }

    const lines = [\`📋 Overzicht — \${new Date().toLocaleDateString("nl-NL")}\`, ""];

    for (const item of items) {
      lines.push(\`🕐 \${item.time}\`);
      lines.push(\`👤 \${item.name}\`);
      lines.push("");
    }

    lines.push(\`Totaal: \${items.length} items vandaag\`);

    await sendToGroup(lines.join("\\n"));
    console.log("✅ Dagelijks overzicht verstuurd");
  } catch (err) {
    console.error("❌ Dagelijks overzicht mislukt:", err);
  }
}

💡 Tip:

Je kunt makkelijk meerdere cron schedules toevoegen, bijv. een avondoverzicht, een weekoverzicht op maandag, of een reminder op vrijdagmiddag voor openstaande zaken.

Handige cron-uitdrukkingen voor veelgebruikte schema's:

Schema Expressie
Elke dag om 08:000 8 * * *
Elke maandag om 09:000 9 * * 1
Elk uur0 * * * *
Elke 30 minuten*/30 * * * *
Eerste dag van de maand0 9 1 * *

Stap 9: Bot 24/7 draaien met PM2

Je wilt dat de bot blijft draaien, ook als je SSH-sessie afsluit of als de server herstart. PM2 is een process manager die hier perfect voor is.

# PM2 installeren
sudo npm install -g pm2

# Bot starten met PM2
pm2 start index.js --name whatsapp-bot

# Opslaan zodat PM2 het onthoudt
pm2 save

# Auto-start na server reboot
pm2 startup
# ↑ Voer het commando uit dat PM2 je geeft

Handige PM2 commando's

pm2 status              # Status van alle processen
pm2 logs whatsapp-bot   # Live logs bekijken
pm2 restart whatsapp-bot # Herstarten
pm2 stop whatsapp-bot   # Stoppen
pm2 monit               # Interactieve monitoring

Een handige extra: automatisch herstarten als de bot te veel geheugen gebruikt (bijv. door een memory leak na een lange uptime):

pm2 start index.js --name whatsapp-bot --max-memory-restart 200M

Stap 10: Monitoring & onderhoud

Health check

De bot heeft een /health endpoint. Gebruik dit om te monitoren of alles nog draait:

curl https://jouw-server.nl/health
{
  "status": "ok",
  "whatsapp": "connected",
  "chatId": "configured",
  "groupValidated": true,
  "uptime": 86400
}

Automatische health checks

Voeg een simpele cron toe aan je VPS om te checken of de bot nog draait:

# Elke 5 minuten checken of de bot nog connected is
*/5 * * * * curl -s http://localhost:3000/health | grep -q '"whatsapp":"connected"' || pm2 restart whatsapp-bot

Externe uptime monitoring (gratis)

Gebruik UptimeRobot (gratis tier) om je /health endpoint van buitenaf te monitoren. Je krijgt een e-mail of pushnotificatie als de bot offline gaat. Instellen duurt 2 minuten.

Waar moet je op letten?

Issue Oplossing
WhatsApp sessie verlopenBot toont nieuwe QR code in logs, scan opnieuw
SIM gedeactiveerd (prepaid)Stuur elke ~2 maanden een SMS om actief te blijven
Server rebootPM2 start de bot automatisch opnieuw
Memory leakPM2 kan automatisch herstarten: --max-memory-restart 200M
whatsapp-web.js updateRegelmatig updaten: npm update whatsapp-web.js

Verder uitbouwen: ideeën voor features

Dit is waar het leuk wordt. Je hebt nu een werkende fundatie die je in alle richtingen kunt uitbreiden. Hier zijn concrete ideeën, van eenvoudig tot geavanceerd.

Commando's uitbreiden

De bot reageert al op mentions. Voeg meer commando's toe:

if (body.includes("status")) {
  await chat.sendMessage("✅ Bot is online en actief!");
  return;
}

if (body.includes("overzicht")) {
  await sendDailyOverview();
  return;
}

if (body.includes("help")) {
  await chat.sendMessage(
    "🤖 *Beschikbare commando's*\\n\\n" +
    "📊 *status* — Bot status\\n" +
    "📋 *overzicht* — Overzicht van vandaag\\n" +
    "❓ *help* — Dit menu"
  );
  return;
}

Meerdere groepen ondersteunen

Je bot hoeft niet beperkt te zijn tot één groep. Breid de configuratie uit met een whitelist van meerdere chat-IDs:

// .env
CHAT_IDS=120363XXXXXXXXX@g.us,120363YYYYYYYYY@g.us

// index.js
const CHAT_IDS = (process.env.CHAT_IDS || "").split(",").filter(Boolean);

async function sendToGroups(message, chatIds = CHAT_IDS) {
  for (const chatId of chatIds) {
    await client.sendMessage(chatId, message);
  }
}

Berichten met opmaak

WhatsApp ondersteunt beperkte markdown:

await sendToGroup(
  "*Vette tekst* voor titels\\n" +
  "_Schuine tekst_ voor nadruk\\n" +
  "~Doorstreept~\\n" +
  "\`\`\`code blok\`\`\`\\n" +
  "• Bullet points doe je gewoon met een streepje of bolletje"
);

Afbeeldingen versturen

const { MessageMedia } = require("whatsapp-web.js");

// Vanuit een URL
const mediaFromUrl = await MessageMedia.fromUrl("https://example.com/afbeelding.jpg");
await client.sendMessage(CHAT_ID, mediaFromUrl, { caption: "📸 Screenshot van het dashboard" });

// Vanuit een lokaal bestand
const mediaFromFile = MessageMedia.fromFilePath("/pad/naar/afbeelding.png");
await client.sendMessage(CHAT_ID, mediaFromFile);

Dit is handig voor het versturen van grafieken, screenshots, of automatisch gegenereerde rapporten.

Inkomende berichten verwerken en opslaan

Sla alle inkomende berichten op in een database voor analyse of logging:

const sqlite3 = require("better-sqlite3");
const db = sqlite3("berichten.db");

db.exec(\`CREATE TABLE IF NOT EXISTS berichten (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  van TEXT,
  bericht TEXT,
  tijdstip DATETIME DEFAULT CURRENT_TIMESTAMP
)\`);

client.on("message", async (msg) => {
  const chat = await msg.getChat();
  if (!chat.isGroup || chat.id._serialized !== CHAT_ID) return;

  db.prepare("INSERT INTO berichten (van, bericht) VALUES (?, ?)")
    .run(msg.from, msg.body);
});

Rate limiting op de webhook

Voorkom misbruik van je webhook door rate limiting toe te voegen:

npm install express-rate-limit
const rateLimit = require("express-rate-limit");

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minuut
  max: 30,             // max 30 requests per minuut
  message: { error: "Te veel requests, probeer later opnieuw" }
});

app.use("/webhook", limiter);

Integratie met externe diensten

De webhook-aanpak maakt het eenvoudig om de bot te koppelen aan vrijwel elk systeem:

  • Supabase / PostgreSQL: stuur een bericht als er een nieuwe rij in een tabel wordt ingevoegd via database triggers
  • WooCommerce / Shopify: nieuwe orders, lage voorraad meldingen
  • Stripe: betalingsbevestigingen
  • Google Calendar: reminders voor afspraken (via Google Apps Script)
  • Zapier / Make: koppel aan honderden diensten zonder code
  • Eigen API: roep de webhook aan vanuit je eigen applicatie

Wekelijks of maandelijks rapport

// Elke maandag om 08:00 — weekoverzicht
cron.schedule("0 8 * * 1", async () => {
  const stats = await getWeekStats();
  await sendToGroup(
    \`📊 *Weekoverzicht*\\n\\n\` +
    \`Totaal afgelopen week: \${stats.total}\\n\` +
    \`Beste dag: \${stats.bestDay}\\n\` +
    \`Verwacht deze week: \${stats.forecast}\`
  );
}, { timezone: "Europe/Amsterdam" });

Prepaid SIM actief houden via cron

Als je voor KPN Prepaid hebt gekozen, kun je de SIM actief houden door er automatisch iets mee te doen. Het versturen van een bericht via de bot zelf telt daarvoor. Voeg een maandelijkse "keepalive" cron toe:

// Eerste dag van de maand om 12:00 — SIM keepalive
cron.schedule("0 12 1 * *", async () => {
  console.log("📶 Maandelijkse SIM keepalive...");
  await sendToGroup("🤖 Maandelijkse check — alles draait nog! ✅");
}, { timezone: "Europe/Amsterdam" });

Checklist: snel overzicht

📋 Setup Checklist

  • ☐ VPS besteld (TransIP / Hetzner / etc.)
  • ☐ SIM-kaart geactiveerd + WhatsApp geregistreerd
  • ☐ Bot-nummer toegevoegd aan groepschat
  • ☐ Node.js 20 LTS geïnstalleerd op VPS
  • ☐ Chromium geïnstalleerd op VPS
  • ☐ Bot code geschreven (index.js)
  • ☐ .env ingevuld met WEBHOOK_SECRET
  • ☐ QR code gescand, bot gekoppeld
  • ☐ CHAT_ID ingesteld en gevalideerd
  • ☐ Nginx reverse proxy + SSL geconfigureerd
  • ☐ PM2 ingesteld voor 24/7 uptime
  • ☐ Health check cron actief
  • ☐ UptimeRobot monitoring ingesteld (optioneel)

Bronnen