How to read group info in PHP with the Wapito WhatsApp API

List the groups, read one in detail, rename or re-describe it when your own data changes, and follow edits on the webhook.

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.

With PHP the directory sync is a CLI job built on Guzzle: page the list with a do-while, fetch the details you need, and PATCH only when your CRM's name for the group differs from the one on WhatsApp. Guzzle's 4xx exceptions map cleanly onto the outcomes: 404 means the number was removed, 403 means it is no longer an admin.

Before you start

  • composer require guzzlehttp/guzzle
  • getenv('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

  1. List the groups this number is in

    Page through the list and store the ids as strings. This list is the ground truth for which groups your automation can act on, and it changes whenever someone adds or removes the linked number.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('GET', 'https://api.wapito.com/v1/groups?count=50&offset=0', [
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
      ],
    ]);
    
    echo $response->getBody();

    In PHP page the list with a do-while that follows $data['next'] ?? null and yields each group from a generator function, storing the ids as strings; array_column($page['items'], 'id') collects a page's ids in one line. A 32-bit build cannot hold these ids as integers, so never cast.

    API reference for this step
  2. Read one group in detail

    The group object carries the subject, description, creation time, current settings and the participant roll with roles. Fetch it before a write so you are acting on the present state rather than on a cached copy.

    <?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 the detail read is a GET decoded with json_decode($body, true); keep the array for the comparison step and read the subject and description with the null coalescing operator in case a field is absent. A ClientException with status 404 means the group is gone from the number's view.

    API reference for this step
  3. Rename or re-describe the group

    Subject and description changes are visible to every member as a system message, so make them deliberately. A description that carries the rules and an opt-out route does more for your ban risk than any clever pacing.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('PATCH', 'https://api.wapito.com/v1/groups/120363041234567890@g.us', [
      'body' => '{"subject":"Acme Launch Team","description":"Launch week: daily standup at 09:30."}',
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
        'Content-Type' => 'application/json',
      ],
    ]);
    
    echo $response->getBody();

    In PHP rename with a PATCH whose body is json_encode of only the fields that differ, computed with a simple !== comparison against the array from the previous step. Pass JSON_UNESCAPED_UNICODE, otherwise an accented subject arrives as escaped sequences that WhatsApp displays verbatim.

    API reference for this step
  4. Follow changes over the webhook

    Renames, description edits and setting changes made by any admin arrive as group events, which is how a cached directory of groups stays accurate without polling the list endpoint.

    Arrives on your webhook as groups.

    In PHP the Slim route decodes the groups event and updates the cached row keyed on $payload['data']['id'], returning the 200 before anything slow. Store the raw event JSON as well, because an audit question about who renamed a group is answered from that column and not from the current state.

The whole script

Every step above in one runnable file. Save it as group-info.php, put your token in the environment, and run it.

<?php
// Group Info with the Wapito WhatsApp API.
//
// List the groups, read one in detail, rename or re-describe it when your own data changes, and follow edits on the webhook.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   php group-info.php
require __DIR__ . '/vendor/autoload.php';

$baseUrl = 'https://api.wapito.com/v1';
$token = getenv('WAPITO_TOKEN');

// --- List the groups this number is in ---
$client = new \GuzzleHttp\Client();

$response = $client->request('GET', $baseUrl . '/groups?count=50&offset=0', [
  'headers' => [
    'Authorization' => 'Bearer ' . $token,
  ],
]);

echo $response->getBody();

// --- Read one group in detail ---
$client = new \GuzzleHttp\Client();

$response = $client->request('GET', $baseUrl . '/groups/120363041234567890@g.us', [
  'headers' => [
    'Authorization' => 'Bearer ' . $token,
  ],
]);

echo $response->getBody();

// --- Rename or re-describe the group ---
$client = new \GuzzleHttp\Client();

$response = $client->request('PATCH', $baseUrl . '/groups/120363041234567890@g.us', [
  'body' => '{"subject":"Acme Launch Team","description":"Launch week: daily standup at 09:30."}',
  '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();
Every event, its payload and the retry rules

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

  • json_encode without JSON_UNESCAPED_UNICODE turns an emoji or an accented letter in the subject into a \u escape the API stores literally; set the flag on every body you build.
  • Under a web request the sync hits max_execution_time halfway through a long list and leaves the table half updated; run it from the CLI or with set_time_limit(0) in a worker.
  • array_diff on nested arrays compares them as the string 'Array' and reports no change; compare the individual subject and description strings instead.

Frequently asked questions

Why is a group missing from the list?

Either the linked number is not in it, or the session has not finished syncing. A newly paired number receives its groups over a short period rather than instantly, so a directory built in the first minutes after pairing will be incomplete. Re-read it once the session reports itself healthy.

Can I read a group my number is not in?

Only its public metadata, and only if you hold an invite code for it - resolving a code returns the name, size and owner without joining. Beyond that, a group is invisible to a number that is not a member, which is a deliberate part of how WhatsApp works.

Does the group object include every participant?

It includes the participant roll with each member's role, which is what most automations need. For very large groups, prefer the dedicated participants endpoint so you can page through the membership instead of pulling one enormous object on every read.

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.