-
Notifications
You must be signed in to change notification settings - Fork 2
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.
__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_TOKENWHATSAPP_CLOUD_PHONE_NUMBER_ID
Relevant optional env keys:
WHATSAPP_CLOUD_API_VERSIONWHATSAPP_CLOUD_API_BASE_URLWHATSAPP_CLOUD_TIMEOUTWHATSAPP_CLOUD_CONNECT_TIMEOUTWHATSAPP_CLOUD_WEBHOOK_VERIFY_TOKENWHATSAPP_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.
Sends a plain text WhatsApp message.
Supported top-level options handled by Pair:
preview_urlrecipient_typecontextreply_to_message_idbiz_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'],
],
],
]
);Sends one of the supported media message types:
audiodocumentimagestickervideo
The $media array must contain exactly one of:
-
idfor previously uploaded media -
linkfor externally hosted media
Optional keys:
caption-
filenamefor 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',
]);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',
],
]);Uploads a local file to Meta storage and returns the API response, including the media ID.
This is usually paired with sendMedia().
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.modehub.verify_tokenhub.challenge
from the current query string.
Validates the X-Hub-Signature-256 header against the raw payload using WHATSAPP_CLOUD_APP_SECRET.
Decodes the raw webhook JSON and throws on empty or invalid bodies.
Flattens Meta's nested webhook payload into a normalized list of events.
Pair currently emits:
messagestatusraw
Each event also includes metadata such as entry_id, change_field, metadata, contacts, and the original raw change payload.
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.
}
}If your API controller extends ApiController or CrudController, Pair already exposes:
GET /api/whatsappWebhookPOST /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.
-
accessTokenSet(): booltells you whether outbound API calls can be made. -
phoneNumberIdSet(): booltells you whether messaging endpoints are fully configured. -
webhookVerifyTokenSet(): booltells you whether challenge verification is configured. -
webhookAppSecretSet(): booltells you whether signature verification is configured.
- Outbound messaging and media methods require both access token and phone number ID.
-
verifyWebhookChallenge()andverifyWebhookSignature()are intentionally separate because Meta uses different secrets for challenge validation and request signing. -
sendMedia()rejects payloads that contain bothidandlink, 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.