[{"data":1,"prerenderedAt":162},["ShallowReactive",2],{"pseo:languages:python":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, 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\"]","scripting",[12,13,14,15,16],"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",1,"Python",{"api_reference":21,"hub":22,"siblings":23},"\u002Fdocs\u002Fapi\u002F","\u002Fwhatsapp-bot\u002F",[24,27],{"title":25,"url":26},"Node.js","\u002Fwhatsapp-bot\u002Fnodejs\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\u002Fpython\u002F","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","2026-09-15","Python WhatsApp Bot: API, Webhooks & 19 Tutorials | Wapito","python","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\u002Fpython\u002F",{"feature":59,"title":60,"url":61},"group-participants","Group Participants","\u002Fwhatsapp-api\u002Fgroup-participants\u002Fpython\u002F",{"feature":63,"title":64,"url":65},"group-admins","Group Admins","\u002Fwhatsapp-api\u002Fgroup-admins\u002Fpython\u002F",{"feature":67,"title":68,"url":69},"group-invite-link","Group Invite Link","\u002Fwhatsapp-api\u002Fgroup-invite-link\u002Fpython\u002F",{"feature":71,"title":72,"url":73},"group-settings","Group Settings","\u002Fwhatsapp-api\u002Fgroup-settings\u002Fpython\u002F",{"feature":75,"title":76,"url":77},"group-join-requests","Group Join Requests","\u002Fwhatsapp-api\u002Fgroup-join-requests\u002Fpython\u002F",{"feature":79,"title":80,"url":81},"group-info","Group Info","\u002Fwhatsapp-api\u002Fgroup-info\u002Fpython\u002F",{"feature":83,"title":84,"url":85},"leave-group","Leave Group","\u002Fwhatsapp-api\u002Fleave-group\u002Fpython\u002F",{"feature":87,"title":88,"url":89},"send-group-message","Send Group Message","\u002Fwhatsapp-api\u002Fsend-group-message\u002Fpython\u002F",{"feature":91,"title":92,"url":93},"group-icon","Group Icon","\u002Fwhatsapp-api\u002Fgroup-icon\u002Fpython\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\u002Fpython\u002F",{"feature":105,"title":106,"url":107},"post-to-channel","Post to Channel","\u002Fwhatsapp-api\u002Fpost-to-channel\u002Fpython\u002F",{"feature":109,"title":110,"url":111},"follow-channels","Follow Channels","\u002Fwhatsapp-api\u002Ffollow-channels\u002Fpython\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\u002Fpython\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\u002Fpython\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\u002Fpython\u002F",{"feature":142,"title":143,"url":144},"lid-to-phone","LID to Phone","\u002Fwhatsapp-api\u002Flid-to-phone\u002Fpython\u002F",{"feature":146,"title":147,"url":148},"profile-picture","Profile Picture","\u002Fwhatsapp-api\u002Fprofile-picture\u002Fpython\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\u002Fpython\u002F",{"install":159,"name":160,"template":161},"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",1790464899381]