Use $vzaps->events() to receive instance events in realtime without exposing a public URL.
For HTTP callbacks, see Webhooks.
In PHP, realtime should run in CLI processes, workers, daemons, Laravel Commands, Symfony Console commands, or queue consumers. Do not keep a WebSocket open inside a normal HTTP request lifecycle.
Common events
| Event | Description |
|---|
Message | New incoming message or message event. |
ReadReceipt | Read/delivery update. |
Presence | User presence. |
ChatPresence | Chat presence. |
HistorySync | History sync. |
Connected | Instance connected to WhatsApp. |
Disconnected | Instance disconnected. |
GroupParticipantsAdd | Participants added to a group. |
GroupParticipantsRemove | Participants removed from a group. |
All | Every subscribed event. |
Install WebSocket transport
composer require textalk/websocket
You can also pass webSocketFactory to VZapsClient for tests or a custom transport.
Subscribe to realtime
use VZaps\Sdk\Models\Realtime\EventSubscribeRequest;
use VZaps\Sdk\Models\Realtime\VZapsEventType;
$subscription = $vzaps->events()->subscribe(new EventSubscribeRequest(
instanceId: 'VZ...',
instanceToken: 'instance-token',
events: [
VZapsEventType::Message,
VZapsEventType::ReadReceipt,
VZapsEventType::Connected,
VZapsEventType::Disconnected,
],
reconnect: true,
maxRetries: 10,
retryDelayMs: 1000,
));
Return: EventSubscription — object with on(), close(), and automatic reconnect when configured.
Options:
| Field | Type | Required | Description |
|---|
instanceId | string | Yes | Instance to monitor. |
instanceToken | string | Yes | Instance token. |
events | VZapsEventType[] or string[] | No | Event list. If omitted, uses events subscribed on the instance. |
reconnect | boolean | No | Reconnect automatically. Default: true. |
maxRetries | int | No | Maximum retry attempts. |
retryDelayMs | int | No | Delay between attempts. |
lastEventId | string | No | Resume cursor. |
Register handlers
$subscription->on(VZapsEventType::Message, function ($event): void {
echo $event->id . PHP_EOL;
echo $event->instanceId . PHP_EOL;
print_r($event->data);
});
$subscription->on(VZapsEventType::All, function ($event): void {
echo 'Received event: ' . $event->type . PHP_EOL;
});
$subscription->onError(function ($error): void {
error_log('Realtime error: ' . $error->getMessage());
});
Close subscription
Return: Promise<void> after the WebSocket closes.
In CLI processes:
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, function () use ($subscription): void {
$subscription->close();
exit(0);
});
}
$subscription->awaitClose();
Event envelope
Each event received by the SDK has this shape:
{
"id": "evt_01J...",
"type": "Message",
"instance_id": "VZ...",
"created_at": "2026-06-23T22:57:17.000Z",
"data": {
"type": "Message",
"media_url": "https://cdn.example.com/media/image.jpg"
}
}
Fields:
| Field | Description |
|---|
id | Event identifier. Use for deduplication. |
type | Event type. |
instance_id | Source instance in the original payload. |
created_at | Event creation time in the original payload. |
data | Event payload. |
data.media_url | Media URL when the incoming event contains media and the platform provides the file. |
On the PHP VZapsEvent object, the main shortcuts are $event->id, $event->type, $event->instanceId, $event->createdAt, $event->data, and $event->raw.
Delivery and ack
Delivery is at-least-once. Your app should process events idempotently.
After the handler finishes, the SDK sends ack automatically.
Recommendations:
- save
event.id if your automation performs external side effects;
- ignore events that were already processed;
- use
lastEventId when reconnecting if you want to reduce gaps;
- keep handlers fast and move long-running work to your own queue.
Realtime or webhook?
| Scenario | Recommendation |
|---|
| Bot, dashboard, or app with immediate consumption | Realtime |
| Backend with public URL and HTTP pipeline | Webhook |
| You do not want to expose a public URL | Realtime |
| You want to reprocess deliveries via logs | Webhook |