How to add group members in PHP with the Wapito WhatsApp API

List who is actually in the group, add the missing people, remove the ones who left, and reconcile from the participants 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.

In PHP the membership sweep runs as a Composer script from the CLI, never from a web request, because a group of a few hundred people means many sequential Guzzle calls and a 30-second execution limit would cut the loop in half. Guzzle raises on 4xx, so the removal loop needs a catch around each call to survive a 404 for someone already gone.

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. Read the current membership

    Always start from the real list rather than from your own copy. Members join by link, leave on their own, and are removed by other admins, so a database that has not seen a participants event in a while is usually out of date.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('GET', 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants', [
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
      ],
    ]);
    
    echo $response->getBody();

    In PHP fetch the roll with $client->request('GET', ...) and decode with json_decode($body, true); the result is a plain array of arrays, so array_column($members, 'id') gives you the identities in one call and array_diff in both directions gives you the two work lists.

    API reference for this step
  2. Add the people who are missing

    Send the numbers in a single call and read the per-participant result. Some will be added, some will be invited instead because their privacy settings forbid direct adds, and some will fail outright - the response says which is which.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('POST', 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants', [
      'body' => '{"participants":["+15551234567","+15559876543"]}',
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
        'Content-Type' => 'application/json',
      ],
    ]);
    
    echo $response->getBody();

    In PHP post the numbers as a JSON body and treat the decoded reply as the real result: an array with one row per number, each carrying a status. foreach over it and write the status to your table; a row marked invited needs a follow-up, and failed usually means the number is not on WhatsApp.

    API reference for this step
  3. Remove someone who has left the team

    Removal is immediate and the person sees that they were removed. Do it from a scheduled job that reads your own source of truth, so nobody is removed because of a transient CRM sync failure.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('DELETE', 'https://api.wapito.com/v1/groups/120363041234567890@g.us/participants/+15551234567', [
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
      ],
    ]);
    
    echo $response->getStatusCode();

    In PHP each removal is a DELETE request with the participant id passed through rawurlencode() into the path. Wrap the call in try/catch for GuzzleHttp\Exception\ClientException and continue on a 404, because a person who left five minutes ago is not an error worth stopping the sweep for; use usleep between calls.

    API reference for this step
  4. Reconcile from the webhook

    Every join, leave, add and remove arrives as an event carrying the action and the participant, who may be a linked identity rather than a phone number. Apply it to your own records so the two never drift apart.

    Arrives on your webhook as groups.participants.

    In PHP the Slim handler decodes the raw body and reads $payload['data']['action'] and $payload['data']['participant']; use the null coalescing operator for the phone number, which is absent when only a linked identity is known. Insert or delete the row, return the 200, and keep the handler free of anything that talks to the network.

The whole script

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

<?php
// Group Participants with the Wapito WhatsApp API.
//
// List who is actually in the group, add the missing people, remove the ones who left, and reconcile from the participants webhook.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   php group-participants.php
require __DIR__ . '/vendor/autoload.php';

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

// --- Read the current membership ---
$client = new \GuzzleHttp\Client();

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

echo $response->getBody();

// --- Add the people who are missing ---
$client = new \GuzzleHttp\Client();

$response = $client->request('POST', $baseUrl . '/groups/120363041234567890@g.us/participants', [
  'body' => '{"participants":["+15551234567","+15559876543"]}',
  'headers' => [
    'Authorization' => 'Bearer ' . $token,
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();

// --- Remove someone who has left the team ---
$client = new \GuzzleHttp\Client();

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

echo $response->getStatusCode();

Receive the webhook

Slim 4 receiver for groups.participants — 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.participants'];

/** 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_decode with assoc true turns the per-participant results into nested arrays, and a foreach that reads $row->status on an array is a warning and a null, so every member looks failed. Read $row['status'].
  • Running the sweep under Apache means the request body limit and the execution timeout both apply; the CLI SAPI has neither, and a supervisor-managed worker is where this script belongs.
  • Casting a participant id with (int) to use it as an array key truncates it on 32-bit PHP and changes it on 64-bit when a linked identity is not numeric at all. Use the string as the key.

Frequently asked questions

Why are some people invited instead of added?

WhatsApp lets everyone choose who may add them to groups. If a person has restricted that to their contacts, an add from an unknown number becomes an invitation they have to accept. The API reports this per participant, so your code can tell the difference between someone who is in the group and someone who has merely been asked.

Is there a limit to how many I can add at once?

The group itself has a member ceiling set by WhatsApp, and practical experience says that adding many people in quick succession draws attention regardless of the ceiling. Add in small batches with a pause between them, and if the group is large, publish an invite link and let people join at their own pace.

Can I re-add someone who left?

Yes, technically, but think about whether you should. Someone who left a group and is immediately put back in is very likely to report the number, and repeated re-adds of the same person are a strong abuse signal. Send them the invite link instead and let them decide.

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.