Skip to content

WhatsAppCloudClient

Viames Marino edited this page Mar 26, 2026 · 1 revision

Pair framework: WhatsAppCloudClient

Pair\Services\WhatsAppCloudClient is Pair's wrapper for Meta WhatsApp Business Platform / Cloud API.

It covers three areas that frequently appear together in real projects:

  • outbound messaging
  • media upload and download
  • webhook verification and event normalization

That makes it useful both in application services and in inbound webhook endpoints.

Constructor

__construct(?string $accessToken = null, ?string $phoneNumberId = null, ?string $apiVersion = null, ?string $apiBaseUrl = null, ?int $timeout = null, ?int $connectTimeout = null, ?string $webhookVerifyToken = null, ?string $appSecret = null)

Builds the client from explicit arguments or .env.

Required for outbound messaging and media API calls:

  • WHATSAPP_CLOUD_ACCESS_TOKEN
  • WHATSAPP_CLOUD_PHONE_NUMBER_ID

Relevant optional env keys:

  • WHATSAPP_CLOUD_API_VERSION
  • WHATSAPP_CLOUD_API_BASE_URL
  • WHATSAPP_CLOUD_TIMEOUT
  • WHATSAPP_CLOUD_CONNECT_TIMEOUT
  • WHATSAPP_CLOUD_WEBHOOK_VERIFY_TOKEN
  • WHATSAPP_CLOUD_APP_SECRET

Webhook-only endpoints can instantiate the client without access token and phone number ID, as long as they only use challenge validation and signature verification.

Main methods

sendText(string $to, string $body, array $options = []): array

Sends a plain text WhatsApp message.

Supported top-level options handled by Pair:

  • preview_url
  • recipient_type
  • context
  • reply_to_message_id
  • biz_opaque_callback_data
use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Send a simple operational update to a customer.
$wa->sendText('393331234567', 'Order #1234 is ready for pickup.', [
    'preview_url' => false,
]);

Replying to a previous inbound message:

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Reply inside the same WhatsApp thread using the message context.
$wa->sendText('393331234567', 'We have received your request.', [
    'reply_to_message_id' => $incomingMessageId,
]);

sendTemplate(string $to, string $templateName, string $languageCode, array $components = [], array $options = []): array

Sends a template message.

This is the most common production flow for notifications that must follow approved WhatsApp templates.

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Send an approved template with one body parameter.
$wa->sendTemplate(
    '393331234567',
    'order_update',
    'en_US',
    [
        [
            'type' => 'body',
            'parameters' => [
                ['type' => 'text', 'text' => '#1234'],
            ],
        ],
    ]
);

sendMedia(string $to, string $mediaType, array $media, array $options = []): array

Sends one of the supported media message types:

  • audio
  • document
  • image
  • sticker
  • video

The $media array must contain exactly one of:

  • id for previously uploaded media
  • link for externally hosted media

Optional keys:

  • caption
  • filename for documents
  • provider
use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();
$upload = $wa->uploadMedia('/tmp/invoice-1234.pdf', 'application/pdf');

// Send the uploaded PDF as a document message.
$wa->sendMedia('393331234567', 'document', [
    'id' => $upload['id'],
    'filename' => 'invoice-1234.pdf',
    'caption' => 'Invoice #1234',
]);

Sending an image by external URL:

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Use a public link instead of uploading the asset first.
$wa->sendMedia('393331234567', 'image', [
    'link' => 'https://example.com/public/banner.jpg',
    'caption' => 'Latest campaign',
]);

sendMessage(array $payload): array

Low-level escape hatch for raw WhatsApp message payloads.

Use it when the project needs a message type or top-level structure that is not covered by the small convenience wrappers.

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Send a raw payload when you need full control over the WhatsApp message body.
$wa->sendMessage([
    'to' => '393331234567',
    'type' => 'text',
    'text' => [
        'body' => 'Raw payload example',
    ],
]);

uploadMedia(string $filePath, ?string $mimeType = null, ?string $fileName = null): array

Uploads a local file to Meta storage and returns the API response, including the media ID.

This is usually paired with sendMedia().

getMediaMetadata(string $mediaId): array

downloadMediaContents(string $mediaId): string

downloadMediaToPath(string $mediaId, string $destinationPath): array

deleteMedia(string $mediaId): bool

These methods are used when the application receives inbound media and needs to inspect or store it locally.

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

// Download an inbound attachment after receiving its media ID in the webhook.
$media = $wa->downloadMediaToPath($mediaId, '/tmp/whatsapp-upload.bin');

downloadMediaToPath() returns the metadata plus the local path.

verifyWebhookChallenge(?string $mode = null, ?string $verifyToken = null, ?string $challenge = null): string

Validates Meta's initial webhook challenge.

If arguments are omitted, Pair reads:

  • hub.mode
  • hub.verify_token
  • hub.challenge

from the current query string.

verifyWebhookSignature(string $payload, ?string $signatureHeader = null): bool

assertWebhookSignature(string $payload, ?string $signatureHeader = null): void

Validates the X-Hub-Signature-256 header against the raw payload using WHATSAPP_CLOUD_APP_SECRET.

decodeWebhookPayload(string $payload): array

Decodes the raw webhook JSON and throws on empty or invalid bodies.

extractWebhookEvents(array $payload): array

Flattens Meta's nested webhook payload into a normalized list of events.

Pair currently emits:

  • message
  • status
  • raw

Each event also includes metadata such as entry_id, change_field, metadata, contacts, and the original raw change payload.

Webhook example with the service only

use Pair\Services\WhatsAppCloudClient;

$wa = new WhatsAppCloudClient();

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
    // Meta calls this during webhook verification.
    echo $wa->verifyWebhookChallenge();
    exit;
}

$payload = file_get_contents('php://input') ?: '';

// Validate authenticity before decoding the nested payload.
$wa->assertWebhookSignature($payload);
$data = $wa->decodeWebhookPayload($payload);
$events = $wa->extractWebhookEvents($data);

foreach ($events as $event) {
    if ($event['event'] === 'message') {
        // Handle inbound user messages here.
    }

    if ($event['event'] === 'status') {
        // Handle sent, delivered, read, or failed notifications here.
    }
}

Built-in Pair endpoint

If your API controller extends ApiController or CrudController, Pair already exposes:

  • GET /api/whatsappWebhook
  • POST /api/whatsappWebhook

The framework:

  • verifies the challenge on GET
  • validates the signature on POST
  • decodes the payload
  • normalizes events with extractWebhookEvents()
  • forwards the result to handleWhatsAppWebhook()

Override the hook in your project controller:

<?php

namespace App\Modules\Api;

use Pair\Api\CrudController;

class ApiController extends CrudController {

    protected function handleWhatsAppWebhook(array $events, array $payload): ?array
    {
        foreach ($events as $event) {
            if ($event['event'] === 'message') {
                // Dispatch the inbound message to your application service.
            }
        }

        return ['received' => true];
    }
}

This is usually the cleanest Pair integration because controller code only handles business logic, while the framework takes care of verification and payload normalization.

Secondary methods

  • accessTokenSet(): bool tells you whether outbound API calls can be made.
  • phoneNumberIdSet(): bool tells you whether messaging endpoints are fully configured.
  • webhookVerifyTokenSet(): bool tells you whether challenge verification is configured.
  • webhookAppSecretSet(): bool tells you whether signature verification is configured.

Notes

  • Outbound messaging and media methods require both access token and phone number ID.
  • verifyWebhookChallenge() and verifyWebhookSignature() are intentionally separate because Meta uses different secrets for challenge validation and request signing.
  • sendMedia() rejects payloads that contain both id and link, or neither of them.
  • The current default Graph API version in Pair is v23.0.

See also: Integrations, Configuration file, Env, Request, ApiController, CrudController, PairException.

Clone this wiki locally