[{"data":1,"prerenderedAt":162},["ShallowReactive",2],{"pseo:languages:nodejs":3},{"deploy_targets":4,"env_idiom":9,"family":10,"gotchas":11,"install":17,"language_tier":18,"name":19,"related":20,"seo":30,"slug":44,"tier":45,"tutorial_count":46,"tutorials_by_category":47,"updated":42,"url":38,"webhook_framework":158},[5,6,7,8],"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",[12,13,14,15,16],"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":21,"hub":22,"siblings":23},"\u002Fdocs\u002Fapi\u002F","\u002Fwhatsapp-bot\u002F",[24,27],{"title":25,"url":26},"Python","\u002Fwhatsapp-bot\u002Fpython\u002F",{"title":28,"url":29},"PHP","\u002Fwhatsapp-bot\u002Fphp\u002F",{"breadcrumb":31,"canonical":39,"description":40,"h1":41,"lastmod":42,"title":43},[32,35,37],{"name":33,"url":34},"Home","\u002F",{"name":36,"url":22},"WhatsApp Bot",{"name":19,"url":38},"\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,[48,94,112,122,131,149],{"category":49,"tutorials":53},{"name":50,"slug":51,"url":52},"Groups","groups","\u002Fwhatsapp-api\u002Fgroups\u002F",[54,58,62,66,70,74,78,82,86,90],{"feature":55,"title":56,"url":57},"create-group","Create Group","\u002Fwhatsapp-api\u002Fcreate-group\u002Fnodejs\u002F",{"feature":59,"title":60,"url":61},"group-participants","Group Participants","\u002Fwhatsapp-api\u002Fgroup-participants\u002Fnodejs\u002F",{"feature":63,"title":64,"url":65},"group-admins","Group Admins","\u002Fwhatsapp-api\u002Fgroup-admins\u002Fnodejs\u002F",{"feature":67,"title":68,"url":69},"group-invite-link","Group Invite Link","\u002Fwhatsapp-api\u002Fgroup-invite-link\u002Fnodejs\u002F",{"feature":71,"title":72,"url":73},"group-settings","Group Settings","\u002Fwhatsapp-api\u002Fgroup-settings\u002Fnodejs\u002F",{"feature":75,"title":76,"url":77},"group-join-requests","Group Join Requests","\u002Fwhatsapp-api\u002Fgroup-join-requests\u002Fnodejs\u002F",{"feature":79,"title":80,"url":81},"group-info","Group Info","\u002Fwhatsapp-api\u002Fgroup-info\u002Fnodejs\u002F",{"feature":83,"title":84,"url":85},"leave-group","Leave Group","\u002Fwhatsapp-api\u002Fleave-group\u002Fnodejs\u002F",{"feature":87,"title":88,"url":89},"send-group-message","Send Group Message","\u002Fwhatsapp-api\u002Fsend-group-message\u002Fnodejs\u002F",{"feature":91,"title":92,"url":93},"group-icon","Group Icon","\u002Fwhatsapp-api\u002Fgroup-icon\u002Fnodejs\u002F",{"category":95,"tutorials":99},{"name":96,"slug":97,"url":98},"Channels","channels","\u002Fwhatsapp-api\u002Fchannels\u002F",[100,104,108],{"feature":101,"title":102,"url":103},"create-channel","Create Channel","\u002Fwhatsapp-api\u002Fcreate-channel\u002Fnodejs\u002F",{"feature":105,"title":106,"url":107},"post-to-channel","Post to Channel","\u002Fwhatsapp-api\u002Fpost-to-channel\u002Fnodejs\u002F",{"feature":109,"title":110,"url":111},"follow-channels","Follow Channels","\u002Fwhatsapp-api\u002Ffollow-channels\u002Fnodejs\u002F",{"category":113,"tutorials":117},{"name":114,"slug":115,"url":116},"Status","status",null,[118],{"feature":119,"title":120,"url":121},"post-status","Post Status","\u002Fwhatsapp-api\u002Fpost-status\u002Fnodejs\u002F",{"category":123,"tutorials":126},{"name":124,"slug":125,"url":116},"Messaging","messaging",[127],{"feature":128,"title":129,"url":130},"send-message-without-template","Send Without Template","\u002Fwhatsapp-api\u002Fsend-message-without-template\u002Fnodejs\u002F",{"category":132,"tutorials":136},{"name":133,"slug":134,"url":135},"Contacts","contacts","\u002Fwhatsapp-api\u002Fcontacts\u002F",[137,141,145],{"feature":138,"title":139,"url":140},"check-number","Check Number","\u002Fwhatsapp-api\u002Fcheck-number\u002Fnodejs\u002F",{"feature":142,"title":143,"url":144},"lid-to-phone","LID to Phone","\u002Fwhatsapp-api\u002Flid-to-phone\u002Fnodejs\u002F",{"feature":146,"title":147,"url":148},"profile-picture","Profile Picture","\u002Fwhatsapp-api\u002Fprofile-picture\u002Fnodejs\u002F",{"category":150,"tutorials":153},{"name":151,"slug":152,"url":116},"Account","account",[154],{"feature":155,"title":156,"url":157},"connect-number-qr-pairing","Connect a Number","\u002Fwhatsapp-api\u002Fconnect-number-qr-pairing\u002Fnodejs\u002F",{"install":159,"name":160,"template":161},"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",1790464899404]