[{"data":1,"prerenderedAt":341},["ShallowReactive",2],{"pseo:languages:all":3},[4,163,252],{"deploy_targets":5,"env_idiom":10,"family":11,"gotchas":12,"install":18,"language_tier":19,"name":20,"related":21,"seo":31,"slug":45,"tier":46,"tutorial_count":47,"tutorials_by_category":48,"updated":43,"url":39,"webhook_framework":159},[6,7,8,9],"Google Cloud Run (container, min instances 0, one process per channel worker)","Railway or Render web service for the Express receiver","Vercel or Netlify Functions for the webhook receiver only - they cannot hold the sender queue open","Fly.io machine with a persistent volume if you queue sends locally","process.env.WAPITO_TOKEN","scripting",[13,14,15,16,17],"fetch never rejects on a 4xx or 5xx. Check response.ok yourself; an await fetch(...) that \"worked\" can still be a 402 plan_required with a JSON error body.","Do not let express.json() run before your webhook route. Signature verification needs the exact raw bytes, so mount express.raw({ type: \"application\u002Fjson\" }) on the webhook path and parse the JSON yourself afterwards.","Node 18+ has global fetch, but node-fetch v2, undici and axios all disagree about how they surface non-2xx responses. Pick one client per project or your error handling will be inconsistent.","Promise.all over a list of recipients defeats the send queue: the API will serialise them anyway and return 429 send_rate_limited for the overflow. Send sequentially with await, or use a queue with concurrency 1 per channel.","Large numbers in JSON.parse lose precision. Treat every WhatsApp id, group id and timestamp that arrives as a string as a string - never Number() it.","npm install express (fetch is built in from Node 18)",1,"Node.js",{"api_reference":22,"hub":23,"siblings":24},"\u002Fdocs\u002Fapi\u002F","\u002Fwhatsapp-bot\u002F",[25,28],{"title":26,"url":27},"Python","\u002Fwhatsapp-bot\u002Fpython\u002F",{"title":29,"url":30},"PHP","\u002Fwhatsapp-bot\u002Fphp\u002F",{"breadcrumb":32,"canonical":40,"description":41,"h1":42,"lastmod":43,"title":44},[33,36,38],{"name":34,"url":35},"Home","\u002F",{"name":37,"url":23},"WhatsApp Bot",{"name":20,"url":39},"\u002Fwhatsapp-bot\u002Fnodejs\u002F","https:\u002F\u002Fwapito.com\u002Fwhatsapp-bot\u002Fnodejs\u002F","Build a WhatsApp bot in Node.js with a REST API that creates groups, posts to Channels and sends without templates. No Meta approval, any number.","Build a WhatsApp Bot in Node.js","2026-09-15","Node.js WhatsApp Bot: API, Webhooks & 19 Tutorials | Wapito","nodejs","t1c",19,[49,95,113,123,132,150],{"category":50,"tutorials":54},{"name":51,"slug":52,"url":53},"Groups","groups","\u002Fwhatsapp-api\u002Fgroups\u002F",[55,59,63,67,71,75,79,83,87,91],{"feature":56,"title":57,"url":58},"create-group","Create Group","\u002Fwhatsapp-api\u002Fcreate-group\u002Fnodejs\u002F",{"feature":60,"title":61,"url":62},"group-participants","Group Participants","\u002Fwhatsapp-api\u002Fgroup-participants\u002Fnodejs\u002F",{"feature":64,"title":65,"url":66},"group-admins","Group Admins","\u002Fwhatsapp-api\u002Fgroup-admins\u002Fnodejs\u002F",{"feature":68,"title":69,"url":70},"group-invite-link","Group Invite Link","\u002Fwhatsapp-api\u002Fgroup-invite-link\u002Fnodejs\u002F",{"feature":72,"title":73,"url":74},"group-settings","Group Settings","\u002Fwhatsapp-api\u002Fgroup-settings\u002Fnodejs\u002F",{"feature":76,"title":77,"url":78},"group-join-requests","Group Join Requests","\u002Fwhatsapp-api\u002Fgroup-join-requests\u002Fnodejs\u002F",{"feature":80,"title":81,"url":82},"group-info","Group Info","\u002Fwhatsapp-api\u002Fgroup-info\u002Fnodejs\u002F",{"feature":84,"title":85,"url":86},"leave-group","Leave Group","\u002Fwhatsapp-api\u002Fleave-group\u002Fnodejs\u002F",{"feature":88,"title":89,"url":90},"send-group-message","Send Group Message","\u002Fwhatsapp-api\u002Fsend-group-message\u002Fnodejs\u002F",{"feature":92,"title":93,"url":94},"group-icon","Group Icon","\u002Fwhatsapp-api\u002Fgroup-icon\u002Fnodejs\u002F",{"category":96,"tutorials":100},{"name":97,"slug":98,"url":99},"Channels","channels","\u002Fwhatsapp-api\u002Fchannels\u002F",[101,105,109],{"feature":102,"title":103,"url":104},"create-channel","Create Channel","\u002Fwhatsapp-api\u002Fcreate-channel\u002Fnodejs\u002F",{"feature":106,"title":107,"url":108},"post-to-channel","Post to Channel","\u002Fwhatsapp-api\u002Fpost-to-channel\u002Fnodejs\u002F",{"feature":110,"title":111,"url":112},"follow-channels","Follow Channels","\u002Fwhatsapp-api\u002Ffollow-channels\u002Fnodejs\u002F",{"category":114,"tutorials":118},{"name":115,"slug":116,"url":117},"Status","status",null,[119],{"feature":120,"title":121,"url":122},"post-status","Post Status","\u002Fwhatsapp-api\u002Fpost-status\u002Fnodejs\u002F",{"category":124,"tutorials":127},{"name":125,"slug":126,"url":117},"Messaging","messaging",[128],{"feature":129,"title":130,"url":131},"send-message-without-template","Send Without Template","\u002Fwhatsapp-api\u002Fsend-message-without-template\u002Fnodejs\u002F",{"category":133,"tutorials":137},{"name":134,"slug":135,"url":136},"Contacts","contacts","\u002Fwhatsapp-api\u002Fcontacts\u002F",[138,142,146],{"feature":139,"title":140,"url":141},"check-number","Check Number","\u002Fwhatsapp-api\u002Fcheck-number\u002Fnodejs\u002F",{"feature":143,"title":144,"url":145},"lid-to-phone","LID to Phone","\u002Fwhatsapp-api\u002Flid-to-phone\u002Fnodejs\u002F",{"feature":147,"title":148,"url":149},"profile-picture","Profile Picture","\u002Fwhatsapp-api\u002Fprofile-picture\u002Fnodejs\u002F",{"category":151,"tutorials":154},{"name":152,"slug":153,"url":117},"Account","account",[155],{"feature":156,"title":157,"url":158},"connect-number-qr-pairing","Connect a Number","\u002Fwhatsapp-api\u002Fconnect-number-qr-pairing\u002Fnodejs\u002F",{"install":160,"name":161,"template":162},"npm install express","Express 5","import crypto from 'node:crypto';\nimport express from 'express';\n\nconst app = express();\nconst SECRET = process.env.{{secret_env}};\nconst EVENTS = new Set([{{#each events}}'{{this}}'{{#unless @last}}, {{\u002Funless}}{{\u002Feach}}]);\n\n\u002F** Checks the {{signature_header}} header: t=\u003Cms>,v1=\u003Chex hmac-sha256>. *\u002F\nfunction verify(raw, header) {\n  if (!header) return false;\n  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));\n  if (!parts.t || !parts.v1) return false;\n  if (Math.abs(Date.now() - Number(parts.t)) > 300_000) return false; \u002F\u002F 5 minute clock skew\n  const expected = crypto.createHmac('sha256', SECRET).update(`${parts.t}.`).update(raw).digest('hex');\n  const a = Buffer.from(expected, 'hex');\n  const b = Buffer.from(parts.v1, 'hex');\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n\napp.post('\u002Fwapito', express.raw({ type: 'application\u002Fjson' }), (req, res) => {\n  if (!verify(req.body, req.get('{{signature_header}}'))) return res.sendStatus(401);\n  const payload = JSON.parse(req.body.toString('utf8'));\n  if (EVENTS.has(payload.event)) console.log(payload.event, payload.data);\n  res.sendStatus(200); \u002F\u002F answer 2xx fast; do the real work in a queue\n});\n\napp.listen(Number(process.env.PORT ?? 3000));\n",{"deploy_targets":164,"env_idiom":169,"family":11,"gotchas":170,"install":176,"language_tier":19,"name":29,"related":177,"seo":181,"slug":190,"tier":46,"tutorial_count":47,"tutorials_by_category":191,"updated":43,"url":30,"webhook_framework":248},[165,166,167,168],"Shared hosting or cPanel with a single public webhook.php endpoint","A VPS managed by Laravel Forge or Ploi, with the sender on a supervisor-managed queue worker","Google Cloud Run using the official php:8.3-cli image","Any Heroku-style buildpack platform for the Slim receiver","getenv('WAPITO_TOKEN')",[171,172,173,174,175],"Guzzle throws a ClientException on a 4xx by default, and the useful part - the API error code - is inside $e->getResponse()->getBody(). Either catch it and decode the body, or pass http_errors => false and branch on the status yourself.","json_decode($raw, true) turns an empty JSON object into an empty PHP array, so is_array() tells you nothing about whether a field arrived. Check array_key_exists() before you read a webhook field.","PHP running behind Apache or shared hosting often gets a request body that has already been consumed. Read the raw webhook body with file_get_contents(\"php:\u002F\u002Finput\") exactly once and pass it around; a second read returns an empty string and signature checks fail.","Integer ids overflow on 32-bit builds. WhatsApp group ids and timestamps are strings in Wapito responses - keep them as strings and never cast with (int).","The default max_execution_time of 30 seconds will kill a bulk loop halfway. Run bulk sends from the CLI SAPI (php send.php), not from a web request.","composer require guzzlehttp\u002Fguzzle",{"api_reference":22,"hub":23,"siblings":178},[179,180],{"title":26,"url":27},{"title":20,"url":39},{"breadcrumb":182,"canonical":186,"description":187,"h1":188,"lastmod":43,"title":189},[183,184,185],{"name":34,"url":35},{"name":37,"url":23},{"name":29,"url":30},"https:\u002F\u002Fwapito.com\u002Fwhatsapp-bot\u002Fphp\u002F","Build a WhatsApp bot in PHP with a REST API that creates groups, posts to Channels and sends without templates. 19 step-by-step tutorials with runnable code.","Build a WhatsApp Bot in PHP","PHP WhatsApp Bot: API, Webhooks & 19 Tutorials | Wapito","php",[192,215,224,229,234,243],{"category":193,"tutorials":194},{"name":51,"slug":52,"url":53},[195,197,199,201,203,205,207,209,211,213],{"feature":56,"title":57,"url":196},"\u002Fwhatsapp-api\u002Fcreate-group\u002Fphp\u002F",{"feature":60,"title":61,"url":198},"\u002Fwhatsapp-api\u002Fgroup-participants\u002Fphp\u002F",{"feature":64,"title":65,"url":200},"\u002Fwhatsapp-api\u002Fgroup-admins\u002Fphp\u002F",{"feature":68,"title":69,"url":202},"\u002Fwhatsapp-api\u002Fgroup-invite-link\u002Fphp\u002F",{"feature":72,"title":73,"url":204},"\u002Fwhatsapp-api\u002Fgroup-settings\u002Fphp\u002F",{"feature":76,"title":77,"url":206},"\u002Fwhatsapp-api\u002Fgroup-join-requests\u002Fphp\u002F",{"feature":80,"title":81,"url":208},"\u002Fwhatsapp-api\u002Fgroup-info\u002Fphp\u002F",{"feature":84,"title":85,"url":210},"\u002Fwhatsapp-api\u002Fleave-group\u002Fphp\u002F",{"feature":88,"title":89,"url":212},"\u002Fwhatsapp-api\u002Fsend-group-message\u002Fphp\u002F",{"feature":92,"title":93,"url":214},"\u002Fwhatsapp-api\u002Fgroup-icon\u002Fphp\u002F",{"category":216,"tutorials":217},{"name":97,"slug":98,"url":99},[218,220,222],{"feature":102,"title":103,"url":219},"\u002Fwhatsapp-api\u002Fcreate-channel\u002Fphp\u002F",{"feature":106,"title":107,"url":221},"\u002Fwhatsapp-api\u002Fpost-to-channel\u002Fphp\u002F",{"feature":110,"title":111,"url":223},"\u002Fwhatsapp-api\u002Ffollow-channels\u002Fphp\u002F",{"category":225,"tutorials":226},{"name":115,"slug":116,"url":117},[227],{"feature":120,"title":121,"url":228},"\u002Fwhatsapp-api\u002Fpost-status\u002Fphp\u002F",{"category":230,"tutorials":231},{"name":125,"slug":126,"url":117},[232],{"feature":129,"title":130,"url":233},"\u002Fwhatsapp-api\u002Fsend-message-without-template\u002Fphp\u002F",{"category":235,"tutorials":236},{"name":134,"slug":135,"url":136},[237,239,241],{"feature":139,"title":140,"url":238},"\u002Fwhatsapp-api\u002Fcheck-number\u002Fphp\u002F",{"feature":143,"title":144,"url":240},"\u002Fwhatsapp-api\u002Flid-to-phone\u002Fphp\u002F",{"feature":147,"title":148,"url":242},"\u002Fwhatsapp-api\u002Fprofile-picture\u002Fphp\u002F",{"category":244,"tutorials":245},{"name":152,"slug":153,"url":117},[246],{"feature":156,"title":157,"url":247},"\u002Fwhatsapp-api\u002Fconnect-number-qr-pairing\u002Fphp\u002F",{"install":249,"name":250,"template":251},"composer require slim\u002Fslim:^4 slim\u002Fpsr7","Slim 4","\u003C?php\n\u002F\u002F public\u002Fwebhook.php - run with: php -S 0.0.0.0:8000 -t public\nrequire __DIR__ . '\u002F..\u002Fvendor\u002Fautoload.php';\n\nuse Psr\\Http\\Message\\ResponseInterface as Response;\nuse Psr\\Http\\Message\\ServerRequestInterface as Request;\nuse Slim\\Factory\\AppFactory;\n\n$secret = getenv('{{secret_env}}');\n$events = [{{#each events}}'{{this}}'{{#unless @last}}, {{\u002Funless}}{{\u002Feach}}];\n\n\u002F** Checks the {{signature_header}} header: t=\u003Cms>,v1=\u003Chex hmac-sha256>. *\u002F\nfunction wapito_verify(string $raw, ?string $header, string $secret): bool\n{\n    if (!$header) {\n        return false;\n    }\n    $parts = [];\n    foreach (explode(',', $header) as $pair) {\n        [$k, $v] = array_pad(explode('=', $pair, 2), 2, null);\n        $parts[$k] = $v;\n    }\n    if (empty($parts['t']) || empty($parts['v1'])) {\n        return false;\n    }\n    if (abs((int) (microtime(true) * 1000) - (int) $parts['t']) > 300000) {\n        return false; \u002F\u002F 5 minute clock skew\n    }\n    $expected = hash_hmac('sha256', $parts['t'] . '.' . $raw, $secret);\n    return hash_equals($expected, $parts['v1']);\n}\n\n$app = AppFactory::create();\n$app->post('\u002Fwapito', function (Request $request, Response $response) use ($secret, $events) {\n    $raw = (string) $request->getBody();\n    if (!wapito_verify($raw, $request->getHeaderLine('{{signature_header}}'), $secret)) {\n        return $response->withStatus(401);\n    }\n    $payload = json_decode($raw, true);\n    if (in_array($payload['event'], $events, true)) {\n        error_log($payload['event'] . ' ' . json_encode($payload['data']));\n    }\n    return $response->withStatus(200); \u002F\u002F answer 2xx fast; do the real work in a queue\n});\n$app->run();\n",{"deploy_targets":253,"env_idiom":258,"family":11,"gotchas":259,"install":265,"language_tier":19,"name":26,"related":266,"seo":270,"slug":279,"tier":46,"tutorial_count":47,"tutorials_by_category":280,"updated":43,"url":27,"webhook_framework":337},[254,255,256,257],"Google Cloud Run (container, scale to zero, one revision per deploy)","Render background worker for the sender, Render web service for the webhook receiver","A 1 GB VPS with systemd for the worker and Caddy in front of the FastAPI receiver","AWS Lambda + API Gateway for the webhook receiver only (the sender needs a long-lived process for queue spacing)","os.environ[\"WAPITO_TOKEN\"]",[260,261,262,263,264],"requests does not raise on a 4xx by itself. Call response.raise_for_status() or check response.ok, otherwise a 429 send_rate_limited silently looks like a successful send and your loop keeps hammering the queue.","json= and data= are not interchangeable. Pass json={...} so requests sets Content-Type: application\u002Fjson and encodes UTF-8 for you; data= with a dict sends form encoding and the API answers 400 invalid_request.","The default requests timeout is None, which means a stalled connection hangs your worker forever. Always pass timeout=(5, 30) and treat a read timeout as \"maybe delivered\" - re-check with the message id before you send again.","A group id such as 120363000000000000@g.us is a string, not a number. Building it with an f-string from an int drops the precision and you get 404 not_found.","Emoji and accented text need no extra work in Python 3, but reading a CSV of numbers with the default encoding on Windows will mangle them - open files with encoding=\"utf-8\".","pip install requests",{"api_reference":22,"hub":23,"siblings":267},[268,269],{"title":20,"url":39},{"title":29,"url":30},{"breadcrumb":271,"canonical":275,"description":276,"h1":277,"lastmod":43,"title":278},[272,273,274],{"name":34,"url":35},{"name":37,"url":23},{"name":26,"url":27},"https:\u002F\u002Fwapito.com\u002Fwhatsapp-bot\u002Fpython\u002F","Build a WhatsApp bot in Python with a REST API that creates groups, posts to Channels and sends without templates. 19 step-by-step tutorials with runnable code.","Build a WhatsApp Bot in Python","Python WhatsApp Bot: API, Webhooks & 19 Tutorials | Wapito","python",[281,304,313,318,323,332],{"category":282,"tutorials":283},{"name":51,"slug":52,"url":53},[284,286,288,290,292,294,296,298,300,302],{"feature":56,"title":57,"url":285},"\u002Fwhatsapp-api\u002Fcreate-group\u002Fpython\u002F",{"feature":60,"title":61,"url":287},"\u002Fwhatsapp-api\u002Fgroup-participants\u002Fpython\u002F",{"feature":64,"title":65,"url":289},"\u002Fwhatsapp-api\u002Fgroup-admins\u002Fpython\u002F",{"feature":68,"title":69,"url":291},"\u002Fwhatsapp-api\u002Fgroup-invite-link\u002Fpython\u002F",{"feature":72,"title":73,"url":293},"\u002Fwhatsapp-api\u002Fgroup-settings\u002Fpython\u002F",{"feature":76,"title":77,"url":295},"\u002Fwhatsapp-api\u002Fgroup-join-requests\u002Fpython\u002F",{"feature":80,"title":81,"url":297},"\u002Fwhatsapp-api\u002Fgroup-info\u002Fpython\u002F",{"feature":84,"title":85,"url":299},"\u002Fwhatsapp-api\u002Fleave-group\u002Fpython\u002F",{"feature":88,"title":89,"url":301},"\u002Fwhatsapp-api\u002Fsend-group-message\u002Fpython\u002F",{"feature":92,"title":93,"url":303},"\u002Fwhatsapp-api\u002Fgroup-icon\u002Fpython\u002F",{"category":305,"tutorials":306},{"name":97,"slug":98,"url":99},[307,309,311],{"feature":102,"title":103,"url":308},"\u002Fwhatsapp-api\u002Fcreate-channel\u002Fpython\u002F",{"feature":106,"title":107,"url":310},"\u002Fwhatsapp-api\u002Fpost-to-channel\u002Fpython\u002F",{"feature":110,"title":111,"url":312},"\u002Fwhatsapp-api\u002Ffollow-channels\u002Fpython\u002F",{"category":314,"tutorials":315},{"name":115,"slug":116,"url":117},[316],{"feature":120,"title":121,"url":317},"\u002Fwhatsapp-api\u002Fpost-status\u002Fpython\u002F",{"category":319,"tutorials":320},{"name":125,"slug":126,"url":117},[321],{"feature":129,"title":130,"url":322},"\u002Fwhatsapp-api\u002Fsend-message-without-template\u002Fpython\u002F",{"category":324,"tutorials":325},{"name":134,"slug":135,"url":136},[326,328,330],{"feature":139,"title":140,"url":327},"\u002Fwhatsapp-api\u002Fcheck-number\u002Fpython\u002F",{"feature":143,"title":144,"url":329},"\u002Fwhatsapp-api\u002Flid-to-phone\u002Fpython\u002F",{"feature":147,"title":148,"url":331},"\u002Fwhatsapp-api\u002Fprofile-picture\u002Fpython\u002F",{"category":333,"tutorials":334},{"name":152,"slug":153,"url":117},[335],{"feature":156,"title":157,"url":336},"\u002Fwhatsapp-api\u002Fconnect-number-qr-pairing\u002Fpython\u002F",{"install":338,"name":339,"template":340},"pip install fastapi uvicorn","FastAPI","import hashlib\nimport hmac\nimport os\nimport time\n\nfrom fastapi import FastAPI, Header, HTTPException, Request\n\napp = FastAPI()\nSECRET = os.environ[\"{{secret_env}}\"].encode()\nEVENTS = [{{#each events}}\"{{this}}\"{{#unless @last}}, {{\u002Funless}}{{\u002Feach}}]\n\n\ndef verify(raw: bytes, header: str | None) -> bool:\n    \"\"\"Checks the {{signature_header}} header: t=\u003Cms>,v1=\u003Chex hmac-sha256>.\"\"\"\n    if not header:\n        return False\n    parts = dict(p.split(\"=\", 1) for p in header.split(\",\") if \"=\" in p)\n    ts, sig = parts.get(\"t\"), parts.get(\"v1\")\n    if not ts or not sig:\n        return False\n    if abs(time.time() * 1000 - int(ts)) > 300_000:  # 5 minute clock skew\n        return False\n    expected = hmac.new(SECRET, ts.encode() + b\".\" + raw, hashlib.sha256).hexdigest()\n    return hmac.compare_digest(expected, sig)\n\n\n@app.post(\"\u002Fwapito\")\nasync def wapito(request: Request, signature: str | None = Header(default=None, alias=\"{{signature_header}}\")):\n    raw = await request.body()\n    if not verify(raw, signature):\n        raise HTTPException(status_code=401, detail=\"bad signature\")\n    payload = await request.json()\n    if payload[\"event\"] in EVENTS:\n        print(payload[\"event\"], payload[\"data\"])\n    return {\"ok\": True}  # answer 2xx fast; do the real work in a queue\n",1790464894220]