Realtime events
There are 4 ways to receive events; pick one based on where your code runs:
| Method | Best for | Notes |
|---|---|---|
Socket.IO namespace /v1 |
Apps and workers that keep a long-lived connection | Fastest; also carries conversation.typing |
SSE GET /api/v1/events/stream |
Scripts, servers that can't install socket.io | Works well through a tunnel |
Polling GET /api/v1/events?since= |
Cron, serverless, Google Apps Script | No connection to keep open |
| Webhook | Servers with a public URL | Deplao POSTs to you and retries automatically (see the "Webhooks" guide) |
The key needs the events:subscribe scope (webhooks need webhooks:manage).
Event structure
json
{
"id": "evt_6b1f0c2d9a4e4c1b8f3a2d10",
"seq": 1532,
"type": "message.received",
"ts": 1760412345678,
"account_id": "0501234567",
"channel": "zalo",
"data": {
"message": { "msg_id": "7712345678901", "thread_id": "8765432109876", "thread_type": "user", "text": "Hi, do you still have size M?", "is_self": false },
"conversation": { "thread_id": "8765432109876", "display_name": "Lan Nguyễn" }
}
}seqkeeps increasing and acts as the "cursor" for catching up on missed events. Store the lastseqyou processed.data.messagehas the same shape asGET .../messages, so one handler works for both.conversation.typingis not stored (seq = 0) and is only available over Socket.IO and SSE.- Full catalog with descriptions:
GET /api/v1/events/types(no key required).
Rules
- A key only receives events for the accounts it has been granted. System-wide events (
payment.received,plugin.*,ping) are only sent to keys without an account restriction. - Type filters:
message.received(exact name),message.*(whole group),*(everything). - Deplao only records events when something can receive them: a key with an events/webhooks scope, an open connection, or an active webhook. Events that happened before you created your first key are not available.
- Messages older than 5 minutes (history sync, loading older messages) do not emit
message.received. - Events are kept for 7 days or the latest 50,000 records; change this in Tools → Apps, API & Webhook → API → Advanced (Công cụ → Ứng dụng, API & Webhook → API → Nâng cao).
- Delivery is at least once: after a reconnect you may receive duplicates. Deduplicate by
idorseq.
Socket.IO (Node.js)
js
// npm i socket.io-client@4
import { io } from 'socket.io-client';
import fs from 'node:fs';
let lastSeq = Number(fs.existsSync('seq.txt') ? fs.readFileSync('seq.txt', 'utf8') : 0);
const socket = io('http://127.0.0.1:9900/v1', {
auth: { apiKey: process.env.DEPLAO_KEY },
transports: ['websocket'],
});
socket.on('hello', ({ seq_now }) => {
// First run starts from now; later runs also receive whatever was missed while disconnected.
socket.emit('subscribe', { types: ['message.*'], since: lastSeq || seq_now }, (ack) => console.log('subscribed', ack));
});
socket.on('event', (ev) => {
if (ev.type === 'message.received') {
console.log(ev.data.conversation.display_name, ':', ev.data.message.text);
}
if (ev.seq) {
lastSeq = ev.seq;
fs.writeFileSync('seq.txt', String(lastSeq));
}
});
socket.on('connect_error', (err) => console.error('Could not connect:', err.message));- Connection errors arrive via
connect_error:UNAUTHORIZED(wrong key),FORBIDDEN_SCOPE(missingevents:subscribe),FORBIDDEN_IP,API_DISABLED. subscribetakes{ types, account_ids, since }. Callsubscribeagain to change the filter;unsubscribeto stop.- Through a tunnel:
io('https://<domain>/v1', ...)(default path/socket.io).
SSE
bash
curl -N -H "Authorization: Bearer dpk_xxx" "http://127.0.0.1:9900/api/v1/events/stream?types=message.*&since=1500"text
id: 1532
event: message.received
data: {"id":"evt_...","seq":1532,"type":"message.received",...}- Reconnect with the
Last-Event-ID: <seq>header or?since=<seq>to receive what you missed. - A
: pingline is sent every 15 seconds to keep the connection alive through proxies and tunnels. - The browser's
EventSourcecannot send anAuthorizationheader. In the browser, use Socket.IO.
Python:
python
import json, requests
r = requests.get(
"http://127.0.0.1:9900/api/v1/events/stream",
params={"types": "message.received"},
headers={"Authorization": "Bearer dpk_xxx"},
stream=True, timeout=(5, 60),
)
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data: "):
ev = json.loads(line[6:])
print(ev["seq"], ev["data"]["message"]["text"])Polling
bash
curl -H "Authorization: Bearer dpk_xxx" "http://127.0.0.1:9900/api/v1/events?since=0&types=message.received&limit=100"json
{ "success": true, "data": [ { "seq": 1532, "type": "message.received" } ], "pagination": { "limit": 100, "has_more": false, "next_since": 1532 }, "request_id": "..." }Call again with since = pagination.next_since. When has_more = true, call again right away; when it is false, wait a few seconds.
Didn't find your answer?
Message us on Telegram