How to get a group invite link in PHP with the Wapito WhatsApp API

Read the code, rotate it on a schedule, resolve unknown codes before accepting, and join by code when you mean to.

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.

Link rotation in PHP is a small Guzzle script that fits well on shared hosting as a cron entry, with the resolve-and-join step exposed through a Slim route when a bot needs it. Guzzle's exceptions do the branching for you: a 404 on a code is a ClientException you catch and treat as a normal answer.

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 invite code

    The code is the tail of a chat.whatsapp.com link. Treat it as a credential rather than as a URL: anyone who holds it can join, so it belongs behind your own redirect rather than pasted into a public page you cannot update.

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

    In PHP read the code with $client->request('GET', ...) and json_decode the body; store $data['code'] as a string in a column your redirect script reads. Keep it out of error_log calls and out of any exception message you rethrow, because on shared hosting those logs are often world-readable inside the account.

    API reference for this step
  2. Rotate it on a schedule

    Revoking generates a fresh code and invalidates every copy of the old link instantly. A nightly rotation means a link that leaks into a forum stops working within a day, without anyone having to notice that it leaked.

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

    In PHP the rotation is a single DELETE request whose decoded reply already carries the new code, so write it to the database in the same statement group. Wrap the request and the update in a try block: if the update fails after the revoke, rethrow, so the cron job exits non-zero and the dead link is noticed.

    API reference for this step
  3. Resolve a code before joining

    Given a code somebody sent you, read the group's name, size and owner before deciding. This is how a bot avoids joining a group it has no business being in.

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

    In PHP resolve a code with a GET on /groups/invite/ . rawurlencode($code) and inspect $data['subject'], $data['size'] and $data['owner'] before deciding. Catch GuzzleHttp\Exception\ClientException for a 404: an expired or mistyped code is an ordinary outcome, not a script failure.

    API reference for this step
  4. Accept the invitation

    Joining by code puts the linked number in the group as an ordinary member. Expect no admin rights, and expect the group's existing members to see the join as a system message.

    <?php
    
    $client = new \GuzzleHttp\Client();
    
    $response = $client->request('POST', 'https://api.wapito.com/v1/groups/invite/accept', [
      'body' => '{"invite_code":"https://chat.whatsapp.com/HkQ2ZpL9vRtAeYm1"}',
      'headers' => [
        'Authorization' => 'Bearer wpt_YOUR_TOKEN',
        'Content-Type' => 'application/json',
      ],
    ]);
    
    echo $response->getBody();

    In PHP join with a POST whose body is json_encode(['code' => $code]); afterwards the linked number is a plain member, so calls that need admin rights will fail with 403 until someone promotes it. Decode the response and store the group id it returns as a string for later use.

    API reference for this step

The whole script

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

<?php
// Group Invite Link with the Wapito WhatsApp API.
//
// Read the code, rotate it on a schedule, resolve unknown codes before accepting, and join by code when you mean to.
//
// Run it with:
//   export WAPITO_TOKEN="wpt_..."
//   php group-invite-link.php
require __DIR__ . '/vendor/autoload.php';

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

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

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

echo $response->getBody();

// --- Rotate it on a schedule ---
$client = new \GuzzleHttp\Client();

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

echo $response->getBody();

// --- Resolve a code before joining ---
$client = new \GuzzleHttp\Client();

$response = $client->request('GET', $baseUrl . '/groups/invite/HkQ2ZpL9vRtAeYm1', [
  'headers' => [
    'Authorization' => 'Bearer ' . $token,
  ],
]);

echo $response->getBody();

// --- Accept the invitation ---
$client = new \GuzzleHttp\Client();

$response = $client->request('POST', $baseUrl . '/groups/invite/accept', [
  'body' => '{"invite_code":"https://chat.whatsapp.com/HkQ2ZpL9vRtAeYm1"}',
  'headers' => [
    'Authorization' => 'Bearer ' . $token,
    'Content-Type' => 'application/json',
  ],
]);

echo $response->getBody();

Receive the webhook

Slim 4 receiver for groups, 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', '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

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

  • A cron entry that runs the rotation script under a different user than php-fpm cannot read the same .env, so getenv('WAPITO_TOKEN') returns false and the revoke silently fails with 401; define the variable for the cron user too.
  • If your redirect script builds the link with the code from a cached array, the cache outlives the rotation and visitors get a revoked link; read the code from the database on every hit, it is one indexed row.

Frequently asked questions

How often should I rotate the invite link?

It depends on where it lives. A link on a private confirmation page can sit for months; a link on a public social profile is worth rotating weekly, because that is where scrapers find them. Rotating behind your own redirect costs nothing to your users, so err on the frequent side.

Can I see who joined through a particular link?

Not directly - WhatsApp reports that someone joined, not which link they used. If you need attribution, use a distinct group per source, or put your own redirect in front of each published link and correlate the click with the join event that follows it.

Does joining by link make my number an admin?

No. Anyone joining by link arrives as an ordinary member, including an automation. If the bot needs to moderate, an existing admin has to promote it after it joins, which is worth building into the onboarding rather than discovering when the first write fails.

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.