How to change group settings in PHP with the Wapito WhatsApp API
A script, a token and an HTTP client is the whole stack. Everything below runs from a file you can execute today, and the same code moves to a worker or a container without changing shape.
For PHP the settings schedule belongs in a cron-driven CLI script rather than a controller: Guzzle reads the group, sends a PATCH that changes one field, and a second cron line sends the reverse. Guzzle throws on a 403, which is the exact signal that the linked number was demoted, so the catch block is where the alerting goes.
Before you start
composer require guzzlehttp/guzzlegetenv('WAPITO_TOKEN')- A channel with a WhatsApp number linked to it, and its API token. Create one in the dashboard — the authentication guide shows where the token goes.
How it works
Read the current settings
The group object carries who may send messages, who may edit the subject and icon, and whether new members need approval. Read before you write so a scheduled job does not flip a setting an admin changed deliberately an hour ago.
<?php $client = new \GuzzleHttp\Client(); $response = $client->request('GET', 'https://api.wapito.com/v1/groups/120363041234567890@g.us', [ 'headers' => [ 'Authorization' => 'Bearer wpt_YOUR_TOKEN', ], ]); echo $response->getBody();In PHP fetch the group with a GET, json_decode the body with assoc true, and read the announcement flag out of the settings array before writing. If it already matches what the schedule wants, return early: a rerun after a crash then costs nothing and posts nothing in the group.
API reference for this stepRestrict posting to admins
Announcement mode is a single field. It is the right default for any group you use to broadcast, because it removes the whole class of accidental replies to hundreds of people.
<?php $client = new \GuzzleHttp\Client(); $response = $client->request('PATCH', 'https://api.wapito.com/v1/groups/120363041234567890@g.us/settings', [ 'body' => '{"messages_admin_only":false,"membership_approval":true}', 'headers' => [ 'Authorization' => 'Bearer wpt_YOUR_TOKEN', 'Content-Type' => 'application/json', ], ]); echo $response->getBody();In PHP restrict posting with $client->request('PATCH', $url, ['body' => json_encode([...]), 'headers' => [...]]) carrying only the changed field; Guzzle sends PATCH bodies like any other. Catch GuzzleHttp\Exception\ClientException, and when getStatusCode() is 403, alert, because the number is no longer an admin.
API reference for this stepOpen it again on a schedule
Flip the same field back when a discussion window opens. Running this from a scheduler is how a large group can hold a question hour without a moderator sitting on the mute button.
<?php $client = new \GuzzleHttp\Client(); $response = $client->request('PATCH', 'https://api.wapito.com/v1/groups/120363041234567890@g.us/settings', [ 'body' => '{"messages_admin_only":false,"membership_approval":true}', 'headers' => [ 'Authorization' => 'Bearer wpt_YOUR_TOKEN', 'Content-Type' => 'application/json', ], ]); echo $response->getBody();In PHP the reopen is the same PATCH with the boolean inverted, so put the request in a function that takes bool $announce and call it from both cron lines. Compute the window with DateTimeImmutable and a DateTimeZone for the group's region, never with date() on the server's default zone.
API reference for this stepRecord the change from the webhook
Settings changes arrive as group events, including ones made by other admins on their phones, so your audit trail reflects what actually happened rather than what your job intended.
Arrives on your webhook as
groups.In PHP the Slim handler decodes the groups event and writes $payload['data'] into an audit table, including the actor field, before returning the 200. A change that arrives with an actor other than the linked number means a human overrode the schedule, and that row is what your ops dashboard should highlight.
The whole script
Every step above in one runnable file. Save it as group-settings.php, put your token in the environment, and run it.
<?php
// Group Settings with the Wapito WhatsApp API.
//
// Read the settings, restrict posting when you are broadcasting, reopen on schedule, and keep an audit trail from the group webhook.
//
// Run it with:
// export WAPITO_TOKEN="wpt_..."
// php group-settings.php
require __DIR__ . '/vendor/autoload.php';
$baseUrl = 'https://api.wapito.com/v1';
$token = getenv('WAPITO_TOKEN');
// --- Read the current settings ---
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $baseUrl . '/groups/120363041234567890@g.us', [
'headers' => [
'Authorization' => 'Bearer ' . $token,
],
]);
echo $response->getBody();
// --- Restrict posting to admins ---
$client = new \GuzzleHttp\Client();
$response = $client->request('PATCH', $baseUrl . '/groups/120363041234567890@g.us/settings', [
'body' => '{"messages_admin_only":false,"membership_approval":true}',
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
],
]);
echo $response->getBody();
// --- Open it again on a schedule ---
$client = new \GuzzleHttp\Client();
$response = $client->request('PATCH', $baseUrl . '/groups/120363041234567890@g.us/settings', [
'body' => '{"messages_admin_only":false,"membership_approval":true}',
'headers' => [
'Authorization' => 'Bearer ' . $token,
'Content-Type' => 'application/json',
],
]);
echo $response->getBody();
Receive the webhook
Slim 4 receiver for groups — it verifies the signature, answers immediately and does the work after. Install it with composer require slim/slim:^4 slim/psr7 and save it as webhook.php.
<?php
// public/webhook.php - run with: php -S 0.0.0.0:8000 -t public
require __DIR__ . '/../vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
$secret = getenv('WAPITO_WEBHOOK_SECRET');
$events = ['groups'];
/** Checks the X-Wapito-Signature header: t=<ms>,v1=<hex hmac-sha256>. */
function wapito_verify(string $raw, ?string $header, string $secret): bool
{
if (!$header) {
return false;
}
$parts = [];
foreach (explode(',', $header) as $pair) {
[$k, $v] = array_pad(explode('=', $pair, 2), 2, null);
$parts[$k] = $v;
}
if (empty($parts['t']) || empty($parts['v1'])) {
return false;
}
if (abs((int) (microtime(true) * 1000) - (int) $parts['t']) > 300000) {
return false; // 5 minute clock skew
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $raw, $secret);
return hash_equals($expected, $parts['v1']);
}
$app = AppFactory::create();
$app->post('/wapito', function (Request $request, Response $response) use ($secret, $events) {
$raw = (string) $request->getBody();
if (!wapito_verify($raw, $request->getHeaderLine('X-Wapito-Signature'), $secret)) {
return $response->withStatus(401);
}
$payload = json_decode($raw, true);
if (in_array($payload['event'], $events, true)) {
error_log($payload['event'] . ' ' . json_encode($payload['data']));
}
return $response->withStatus(200); // answer 2xx fast; do the real work in a queue
});
$app->run();
Errors you may hit
Gotchas
- 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://input") 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.
Where to run it
- 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
Pitfalls in PHP
- date_default_timezone_set() is process-wide and often left at UTC on a VPS, so a window computed with date('H') reopens the group at the wrong hour; use DateTimeImmutable with an explicit DateTimeZone.
- Two cron lines that both run at the same minute after a daylight-saving change can fire the restrict and the reopen back to back; schedule them in the group's zone or check the current value before writing.
- Guzzle's ClientException carries the body, and the API's error code inside it is the useful part; catching \Exception and logging only getMessage() throws that information away.
Frequently asked questions
What settings can I change from the API?
The ones an admin sees on the phone: who may send messages, who may edit the group's subject, icon and description, and whether people joining by link need approval first. Read the group object to see the current values, because other admins can change them at any time.
Does announcement mode stop replies entirely?
It stops non-admins from posting in the group, which is what makes it suitable for broadcasts. People can still react to messages, and they can still message the linked number privately, so plan a path for the replies you do want rather than assuming nobody will try.
Can I make a group members-only invisible to search?
Groups are not searchable on WhatsApp in the first place - they are reachable only through an invite link or a direct add. The nearest control is to revoke the link and require approval, which together mean nobody joins without an admin acting.
Related
Try it on your own number
Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.