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