PHP
Send WhatsApp messages from PHP 8.1+ with the Pingmate\Sdk Composer package, built on Guzzle.
The PHP client lives under the Pingmate\Sdk namespace, targets PHP 8.1 and later, and uses Guzzle for HTTP. It fits cleanly into Laravel, Symfony, or plain PHP.
Install
composer require pingmate/sdkThen load Composer's autoloader if your framework does not already:
require_once __DIR__ . '/vendor/autoload.php';Distribution
The client is at 0.1.0. If it is not yet on your Composer registry, add it as a vcs repository pointing at the package source you were given, then require it at *@dev.
Configure
Set the host and the X-API-Key value on the default configuration. Read the key from the environment, never hard-code it.
use Pingmate\Sdk\Configuration;
use Pingmate\Sdk\Api\MessagesApi;
use Pingmate\Sdk\Api\BulkApi;
$config = Configuration::getDefaultConfiguration()
->setHost('https://pingmate.app')
->setApiKey('X-API-Key', getenv('PINGMATE_API_KEY'));
$messages = new MessagesApi(new GuzzleHttp\Client(), $config);
$bulk = new BulkApi(new GuzzleHttp\Client(), $config);setHost defaults to http://localhost, so point it at your PingMate host (or your white-label domain). Pass your own object implementing GuzzleHttp\ClientInterface to the constructor if you need custom timeouts or middleware.
Examples
Every message type flows through the same sendMessage call. You pick the variant by constructing the matching model class. The examples below assume the $messages client, a recipient $to = '919876543210' (E.164 digits, country code, no leading +), and these imports:
use Pingmate\Sdk\Model\SendRequest;
use Pingmate\Sdk\Model\BulkSendRequest;
use Pingmate\Sdk\Model\TextMessage;
use Pingmate\Sdk\Model\ImageMessage;
use Pingmate\Sdk\Model\VideoMessage;
use Pingmate\Sdk\Model\DocumentMessage;
use Pingmate\Sdk\Model\LocationMessage;
use Pingmate\Sdk\Model\Location;
use Pingmate\Sdk\Model\ContactMessage;
use Pingmate\Sdk\Model\Contact;
use Pingmate\Sdk\Model\ButtonsMessage;
use Pingmate\Sdk\Model\Button;
use Pingmate\Sdk\Model\InteractiveListMessage;
use Pingmate\Sdk\Model\InteractiveList;
use Pingmate\Sdk\Model\InteractiveListSection;
use Pingmate\Sdk\Model\InteractiveListRow;
use Pingmate\Sdk\Model\CarouselMessage;
use Pingmate\Sdk\Model\CarouselCard;
use Pingmate\Sdk\Model\TemplateMessage;
use Pingmate\Sdk\Model\Attachment;Text
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new TextMessage([
'messageType' => 'text',
'text' => 'Hello from the PingMate PHP SDK 👋',
]),
]));Reply in a thread
Set contextId to the ID of the message you are replying to.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new TextMessage([
'messageType' => 'text',
'text' => 'Yes, your order is on the way.',
'contextId' => 'wamid.HBgL...',
]),
]));Image with caption
attachment is a public URL or a media ID from /api/v1/media/upload.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new ImageMessage([
'messageType' => 'image',
'attachments' => new Attachment(['type' => 'image', 'attachment' => 'https://picsum.photos/600/400']),
'text' => 'Photo of the day',
]),
]));Video with caption
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new VideoMessage([
'messageType' => 'video',
'attachments' => new Attachment(['type' => 'video', 'attachment' => 'https://example.com/demo.mp4']),
'text' => 'A 30-second product tour',
]),
]));Document
fileName sets the display name the recipient sees.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new DocumentMessage([
'messageType' => 'document',
'attachments' => new Attachment([
'type' => 'document',
'attachment' => 'https://example.com/invoice.pdf',
'fileName' => 'Invoice-2026-001.pdf',
]),
'text' => 'Here is your invoice',
]),
]));Location
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new LocationMessage([
'messageType' => 'location',
'location' => new Location([
'latitude' => '19.0760',
'longitude' => '72.8777',
'name' => 'Mumbai Office',
'address' => 'Bandra Kurla Complex, Mumbai',
]),
]),
]));Contact card
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new ContactMessage([
'messageType' => 'contact',
'contact' => new Contact([
'name' => 'PingMate Support',
'phoneNumber' => '919876500000',
'organization' => 'PingMate',
]),
]),
]));Quick-reply buttons
Up to three tappable buttons. The buttonPayload comes back to your webhook on tap.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new ButtonsMessage([
'messageType' => 'buttons',
'text' => 'How would you like to proceed?',
'headerText' => 'Order #12345',
'footerText' => 'Reply within 24 hours',
'buttons' => [
new Button(['buttonType' => 'text', 'buttonText' => 'Confirm', 'buttonPayload' => 'confirm_order']),
new Button(['buttonType' => 'text', 'buttonText' => 'Cancel', 'buttonPayload' => 'cancel_order']),
],
]),
]));Call-to-action buttons
url and call buttons open a link or dial a number instead of replying.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new ButtonsMessage([
'messageType' => 'buttons',
'text' => 'Your order has shipped.',
'buttons' => [
new Button(['buttonType' => 'url', 'buttonText' => 'Track order', 'buttonPayload' => 'https://example.com/track/12345']),
new Button(['buttonType' => 'call', 'buttonText' => 'Call support', 'buttonPayload' => '919876500000']),
],
]),
]));Interactive list
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new InteractiveListMessage([
'messageType' => 'interactive_list',
'text' => 'Please select a service',
'headerText' => 'Our Services',
'interactiveList' => new InteractiveList([
'title' => 'Select a service',
'sections' => [
new InteractiveListSection([
'title' => 'Support',
'rows' => [
new InteractiveListRow(['id' => 'billing', 'title' => 'Billing Support', 'description' => 'Invoices and payments']),
new InteractiveListRow(['id' => 'technical', 'title' => 'Technical Support', 'description' => 'Product issues']),
],
]),
new InteractiveListSection([
'title' => 'Sales',
'rows' => [
new InteractiveListRow(['id' => 'demo', 'title' => 'Request a Demo']),
new InteractiveListRow(['id' => 'pricing', 'title' => 'Get Pricing']),
],
]),
],
]),
]),
]));Carousel
A swipeable row of cards, each with its own image, text, and buttons. Up to ten cards.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new CarouselMessage([
'messageType' => 'carousel',
'text' => "This week's bestsellers 🛍️",
'cards' => [
new CarouselCard([
'attachments' => new Attachment(['type' => 'image', 'attachment' => 'https://example.com/serum.jpg']),
'text' => 'Aurora Serum, ₹1,299',
'buttons' => [
new Button(['buttonType' => 'url', 'buttonText' => 'View', 'buttonPayload' => 'https://example.com/p/serum']),
new Button(['buttonType' => 'text', 'buttonText' => 'Add to cart', 'buttonPayload' => 'add_serum']),
],
]),
new CarouselCard([
'attachments' => new Attachment(['type' => 'image', 'attachment' => 'https://example.com/lip-tint.jpg']),
'text' => 'Velvet Lip Tint, ₹699',
'buttons' => [
new Button(['buttonType' => 'url', 'buttonText' => 'View', 'buttonPayload' => 'https://example.com/p/lip-tint']),
new Button(['buttonType' => 'text', 'buttonText' => 'Add to cart', 'buttonPayload' => 'add_tint']),
],
]),
],
]),
]));Template (basic)
Templates are the only message type you can send to open a new conversation. The template must be approved first.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new TemplateMessage([
'messageType' => 'template',
'templateName' => 'hello_world',
'templateLanguage' => 'en_US',
]),
]));Template with media header and variables
headerVariables and bodyVariables fill the {{1}}, {{2}} placeholders in order.
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new TemplateMessage([
'messageType' => 'template',
'templateName' => 'order_complete',
'templateLanguage' => 'en_US',
'headerVariables' => ['ORD-12345'],
'bodyVariables' => ['John', '₹2,499', 'March 15, 2026'],
'attachments' => new Attachment(['type' => 'image', 'attachment' => 'https://example.com/order-confirmation.jpg']),
]),
]));Schedule for later
Add scheduleTime to the request. The format is DD-MM-YYYY:HH-MM in IST, and the wallet is charged when you call. See Scheduling.
$messages->sendMessage(new SendRequest([
'to' => $to,
'scheduleTime' => '25-12-2026:09-30',
'message' => new TextMessage(['messageType' => 'text', 'text' => 'Your festive offer is live.']),
]));Bulk send
One message (commonly a template) to up to 50 recipients, deduplicated server-side.
$bulk->sendBulkMessage(new BulkSendRequest([
'to' => ['919876543210', '919876543211', '919876543212'],
'message' => new TemplateMessage([
'messageType' => 'template',
'templateName' => 'promotion_offer',
'templateLanguage' => 'en_US',
'bodyVariables' => ['30%', 'March 31, 2026'],
]),
]));Workflows
These combine the calls above into the patterns you will actually ship.
Upload media, then send it
Upload large or reused files once. uploadMedia returns an id you reference from any media message instead of a public URL. Limits: images 5 MB, video and audio 16 MB, PDF 100 MB.
use Pingmate\Sdk\Api\MediaApi;
$mediaApi = new MediaApi(new GuzzleHttp\Client(), $config);
$upload = $mediaApi->uploadMedia(new \SplFileObject('catalog.pdf'), 'Spring Catalog');
$mediaId = $upload->getData()->getId();
$messages->sendMessage(new SendRequest([
'to' => $to,
'message' => new DocumentMessage([
'messageType' => 'document',
'attachments' => new Attachment([
'type' => 'document',
'attachment' => $mediaId,
'fileName' => 'Spring Catalog.pdf',
]),
'text' => 'Our spring catalog is here 📖',
]),
]));Bulk campaign with per-recipient results
sendBulkMessage returns one result per recipient. Each carries either an id (queued) or an error, so you can log or retry the failures.
$res = $bulk->sendBulkMessage(new BulkSendRequest([
'to' => ['919876543210', '919876543211', '919800000000'],
'message' => new TemplateMessage([
'messageType' => 'template',
'templateName' => 'promotion_offer',
'templateLanguage' => 'en_US',
'bodyVariables' => ['30%', 'March 31, 2026'],
]),
]));
foreach ($res->getData()->getResults() as $r) {
if ($r->getError() !== null) {
echo "✗ {$r->getTo()}: {$r->getError()}\n";
} else {
echo "✓ {$r->getTo()}: {$r->getId()}\n";
}
}Resilient sending
A send returns 402 Payment Required when the wallet is empty or the plan is inactive. Catch it and surface a clear action instead of crashing the job.
try {
$messages->sendMessage($request);
} catch (\Pingmate\Sdk\ApiException $e) {
if ($e->getCode() === 402) {
throw new \RuntimeException('PingMate wallet is empty. Top up and retry.');
}
throw $e;
}Reference
Every method and model is documented in the package's docs/ directory, alongside the API reference.
How is this guide?


