getheybot / heybot-php
Official PHP client for the Heybot WhatsApp API
Requires
- php: ^8.4
- ext-json: *
- symfony/http-client: ^8.0
Requires (Dev)
- laravel/pint: ^1.29
- pestphp/pest: ^4.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Official PHP client for the Heybot WhatsApp API.
Requirements
- PHP 8.4+
- Composer
Installation
composer require getheybot/heybot-php
Quick Start
use Heybot\Client; $heybot = new Client('your-api-key');
Laravel Octane
This package is safe to use with Laravel Octane.
It holds no static or global mutable state — the only static member,
Client::RESOURCE_MAP, is a const — and Client/HttpClient instances
carry no per-request data, so they can be safely reused across requests in a
long-running Octane worker.
To avoid recreating the underlying HTTP client on every request, bind
Client as a singleton:
// AppServiceProvider::register() $this->app->singleton(Client::class, fn () => new Client(config('services.heybot.key')));
Only avoid baking per-request-varying data (e.g. a per-user API key) into a
singleton at boot — resolve Client fresh instead if the API key needs to
change between requests.
Sending Messages
Every message needs exactly one recipient identifier — either:
to— the recipient's phone number, without a leading+(e.g.'521234567890')recipient— the recipient's Business-Scoped User ID (BSUID)
Providing both, or neither, throws Heybot\Exceptions\InvalidRequestException.
Text
$heybot->message->send([ 'to' => '521234567890', 'type' => 'text', 'text' => ['body' => 'Hello from Heybot!'], ]); // Or, targeting a BSUID instead of a phone number: $heybot->message->send([ 'recipient' => 'BSUID_ABC123', 'type' => 'text', 'text' => ['body' => 'Hello from Heybot!'], ]);
Image
$heybot->message->send([ 'to' => '521234567890', 'type' => 'image', 'image' => [ 'url' => 'https://example.com/photo.jpg', 'caption' => 'Check this out!', ], ]);
Document
$heybot->message->send([ 'to' => '521234567890', 'type' => 'document', 'document' => [ 'url' => 'https://example.com/file.pdf', 'filename' => 'invoice.pdf', 'caption' => 'Your invoice', ], ]);
Template
$heybot->message->send([ 'to' => '521234567890', 'type' => 'template', 'template' => [ 'name' => 'hello_world', 'language' => ['code' => 'en_US'], ], ]);
Webhooks
Use WebhookParser to parse incoming webhook payloads into typed event objects.
use Heybot\Webhook\WebhookParser; // From a raw JSON string (e.g. request body) $event = WebhookParser::fromJson($request->getContent()); // Or from an already-decoded array $event = WebhookParser::parse($payload);
Parsing raw Meta webhooks
If you receive webhooks directly from Meta's WhatsApp Cloud API (rather than
Heybot's normalized format), use MetaWebhookParser instead. Meta batches
webhook calls, so parsing returns an array of events — covering both inbound
messages and message status updates (sent/delivered/read/failed).
parse()/fromJson() accept either the full webhook envelope
({"entry": [...]}) or a single already-unwrapped change fragment
({"value": {...}}), if that's what you're handed.
use Heybot\Webhook\MetaWebhookParser; $events = MetaWebhookParser::fromJson($request->getContent()); // Or: MetaWebhookParser::parse($payload); foreach ($events as $event) { if ($event->isDeliveryStatus()) { match (true) { $event->isSent() => handleSent($event->messageId), $event->isDelivered() => handleDelivered($event->messageId), $event->isRead() => handleRead($event->messageId), $event->isFailed() => handleFailed($event->messageId, $event->error), }; continue; } // $event->isMessage() is true here — it's an IncomingEvent // (TextEvent, ImageEvent, ...), same types WebhookParser produces, // handled the same way (see below). }
Note: Meta's raw webhook only provides a media
idfor audio/image/video/document/sticker messages, not a downloadable URL. For these events,$event->mediaIdis set and$event->urlisnull— fetch the URL yourself via the Media API.WebhookParser(Heybot's normalized format) continues to populate$event->urldirectly.
Common event methods
Every event — from WebhookParser or MetaWebhookParser, message or status —
implements Heybot\Webhook\Events\WebhookEvent:
$event->isMessage(); // bool — true for TextEvent, ImageEvent, ... (not StatusEvent) $event->isDeliveryStatus(); // bool — true only for StatusEvent $event->toArray(); // array<string, mixed> — recursive plain-array representation
Business-Scoped User IDs (BSUID)
When Business-Scoped User IDs are enabled, Meta may withhold the sender's phone number and identify them only by a BSUID instead.
$event->from->phoneNumber; // ?string — null when the phone number is withheld $event->from->bsuid; // ?string — set when BSUID is in use
Always check bsuid as a fallback identifier when phoneNumber is null.
StatusEvent
$event->messageId; // string — ID of the message this status is about $event->status; // 'sent'|'delivered'|'read'|'failed' $event->recipientId; // string — recipient's WhatsApp ID $event->conversationId; // ?string $event->conversationOrigin; // ?string — e.g. 'user_initiated', 'business_initiated' $event->billable; // ?bool $event->pricingCategory; // ?string $event->error; // ?StatusError — present when status is 'failed' $event->error?->code; // int $event->error?->title; // string $event->error?->message; // ?string $event->error?->details; // ?string $event->isSent(); $event->isDelivered(); $event->isRead(); $event->isFailed();
Handling events
use Heybot\Webhook\Events\TextEvent; use Heybot\Webhook\Events\ImageEvent; use Heybot\Webhook\Events\AudioEvent; use Heybot\Webhook\Events\VideoEvent; use Heybot\Webhook\Events\DocumentEvent; use Heybot\Webhook\Events\StickerEvent; use Heybot\Webhook\Events\ButtonEvent; use Heybot\Webhook\Events\InteractiveEvent; use Heybot\Webhook\Events\LocationEvent; use Heybot\Webhook\Events\ReactionEvent; use Heybot\Webhook\Events\ContactEvent; match (true) { $event instanceof TextEvent => handleText($event->body), $event instanceof ImageEvent => handleImage($event->url, $event->caption), $event instanceof AudioEvent => handleAudio($event->url, $event->voice), $event instanceof VideoEvent => handleVideo($event->url), $event instanceof DocumentEvent => handleDoc($event->filename, $event->url), $event instanceof StickerEvent => handleSticker($event->url, $event->animated), $event instanceof ButtonEvent => handleButton($event->payload), $event instanceof InteractiveEvent => handleInteractive($event), $event instanceof LocationEvent => handleLocation($event->latitude, $event->longitude), $event instanceof ReactionEvent => handleReaction($event->emoji, $event->messageId), $event instanceof ContactEvent => handleContacts($event->contacts), default => null, };
All events share these base properties:
| Property | Type | Description |
|---|---|---|
$event->id |
string |
Unique message ID |
$event->timestamp |
int |
Unix timestamp |
$event->channel |
string |
e.g. "whatsapp" |
$event->type |
string |
Message type |
$event->from->phoneNumber |
?string |
Sender's phone number — null when withheld (see Business-Scoped User IDs) |
$event->from->bsuid |
?string |
Business-Scoped User ID — identifies the sender when phoneNumber is null |
$event->from->displayName |
string |
Sender's display name |
$event->to->phoneNumber |
string |
Recipient phone number |
$event->to->phoneNumberId |
string |
Recipient phone number ID |
Event types
TextEvent
$event->body; // string — message text
AudioEvent
$event->url; // ?string — null when only $event->mediaId is known (see note above) $event->mediaId; // ?string $event->mimeType; // string $event->sha256; // string $event->voice; // bool — true if voice note
ImageEvent / VideoEvent
$event->url; // ?string — null when only $event->mediaId is known (see note above) $event->mediaId; // ?string $event->mimeType; // string $event->sha256; // string $event->caption; // ?string (image only) $event->context?->forwarded; // ?bool $event->context?->frequentlyForwarded; // ?bool
DocumentEvent
$event->url; // ?string — null when only $event->mediaId is known (see note above) $event->mediaId; // ?string $event->filename; // string $event->mimeType; // string $event->sha256; // string $event->caption; // ?string
StickerEvent
$event->url; // ?string — null when only $event->mediaId is known (see note above) $event->mediaId; // ?string $event->mimeType; // string $event->sha256; // string $event->animated; // bool
ButtonEvent
$event->payload; // string — button ID / payload $event->text; // string — button label
InteractiveEvent
$event->interactiveType; // 'list_reply' | 'button_reply' $event->replyId; // string $event->replyTitle; // string $event->replyDescription; // ?string (list_reply only) $event->isListReply(); // bool $event->isButtonReply(); // bool
LocationEvent
$event->latitude; // float $event->longitude; // float $event->address; // string $event->name; // ?string $event->url; // ?string
ReactionEvent
$event->messageId; // string — ID of the message reacted to $event->emoji; // string — empty string means reaction was removed $event->isRemoval(); // bool
ContactEvent
foreach ($event->contacts as $contact) { $contact->name->formattedName; // string $contact->name->firstName; // ?string $contact->name->lastName; // ?string $contact->phones[0]->phone; // string $contact->phones[0]->waId; // ?string $contact->phones[0]->type; // ?string (e.g. 'MOBILE') $contact->emails[0]->email; // string $contact->emails[0]->type; // ?string $contact->addresses[0]->street; // ?string $contact->addresses[0]->city; // ?string $contact->addresses[0]->state; // ?string $contact->addresses[0]->zip; // ?string $contact->addresses[0]->country; // ?string $contact->addresses[0]->countryCode; // ?string $contact->org?->company; // ?string $contact->org?->department; // ?string $contact->org?->title; // ?string $contact->urls[0]->url; // string $contact->urls[0]->type; // ?string $contact->birthday; // ?string (e.g. '1990-01-15') }
Error Handling
use Heybot\Exceptions\AuthenticationException; use Heybot\Exceptions\RateLimitException; use Heybot\Exceptions\InvalidRequestException; use Heybot\Exceptions\ApiException; try { $heybot->message->send([...]); } catch (AuthenticationException $e) { // 401 — invalid API key } catch (RateLimitException $e) { // 429 — too many requests } catch (InvalidRequestException $e) { // 4xx — bad request echo $e->getErrorCode(); // API error code if provided } catch (ApiException $e) { // 5xx or network error }
License
MIT