Hubz API v1 (beta) · Hubz v0.7.0 · 2026-11-06
Webhook: nhận sự kiện tin nhắn Zalo vào CRM
Sự kiện
| Sự kiện | Khi nào |
|---|---|
message.created | Có tin mới trong một hội thoại, của khách hoặc của nhân viên. data.firstInbound là true khi đây là tin đầu tiên khách gửi trong hội thoại: dùng để tạo cơ hội bán hàng, phiếu hỗ trợ mới. |
message.updated | Một tin thay đổi, ví dụ trạng thái gửi. |
conversation.updated | Một hội thoại thay đổi: trạng thái, người phụ trách, tin mới nhất, số tin chưa đọc. |
Đăng ký webhook
PUT /v1/webhook với địa chỉ https của bạn (không trỏ vào địa chỉ nội bộ) và danh sách sự kiện; khóa cần quyền events. Bí mật ký được trả về khi tạo mới, đổi địa chỉ hoặc rotateSecret. POST /v1/webhook/test gửi thử một sự kiện ping; DELETE /v1/webhook gỡ webhook.
curl -X PUT https://api.hubz.vn/v1/webhook \
-H "Authorization: Bearer hbz_live_xxx" \
-H "Content-Type: application/json" \
-d '{"url": "https://crm.example.com/hubz/webhook", "events": ["message.created", "conversation.updated"]}'
Hubz gửi gì
Một request POST JSON { id, type, version, createdAt, data } với các header X-Hubz-Event, X-Hubz-Delivery (id sự kiện, dùng để bỏ trùng) và X-Hubz-Signature:
POST /hubz/webhook
Content-Type: application/json
X-Hubz-Event: message.created
X-Hubz-Delivery: 1042
X-Hubz-Signature: t=1793956502,v1=5d41402abc4b2a76b9719d911017c592…
{
"id": "1042",
"type": "message.created",
"version": 1,
"createdAt": "2026-11-06T09:15:02Z",
"data": {
"accountId": "…",
"message": {"id": "…", "conversationId": "…", "direction": "in", "kind": "text", "text": "…", "status": "sent", "source": "customer"},
"conversation": {"id": "…", "accountId": "…", "threadType": "user", "status": "open", "assigneeId": null},
"contact": {"id": "…", "name": "…", "phone": "…"},
"firstInbound": true
}
}
Trả lời và thử lại
- Trả mã 2xx trong 10 giây. Xử lý lâu thì nhận trước, xử lý sau.
- Lỗi hoặc quá giờ thì Hubz thử lại sau 1 phút, 5 phút, 30 phút, 2 giờ, 12 giờ.
- Lỗi liên tục 24 giờ thì webhook tạm tắt. Sự kiện vẫn lấy được bằng GET /v1/events trong 7 ngày.
- Một sự kiện có thể tới hơn một lần: bỏ trùng theo
X-Hubz-Delivery.
Kiểm chữ ký
X-Hubz-Signature có dạng t=<unix>,v1=<hex>, trong đó v1 = HMAC-SHA256(bí mật, "<t>.<thân request>"). Tính lại trên thân request nguyên gốc (trước khi parse JSON), so sánh an toàn về thời gian, và bỏ request có t lệch quá 5 phút.
Python
import hashlib
import hmac
import time
def verify_hubz_signature(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
"""header: the X-Hubz-Signature value; body: the raw request body, before any JSON parsing."""
parts = dict(item.split("=", 1) for item in header.split(",") if "=" in item)
timestamp, signature = parts.get("t"), parts.get("v1")
if not timestamp or not signature or abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Node.js
import crypto from "node:crypto";
// header: the X-Hubz-Signature value; rawBody: the raw request body (Buffer), before JSON parsing.
export function verifyHubzSignature(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
const timestamp = Number(parts.t);
if (!timestamp || !parts.v1 || Math.abs(Date.now() / 1000 - timestamp) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
PHP
<?php
// $header: the X-Hubz-Signature value; $body: file_get_contents('php://input'), before json_decode.
function hubz_verify_signature(string $secret, string $header, string $body, int $tolerance = 300): bool
{
$parts = [];
foreach (explode(',', $header) as $item) {
[$name, $value] = array_pad(explode('=', $item, 2), 2, '');
$parts[$name] = $value;
}
if (empty($parts['t']) || empty($parts['v1']) || abs(time() - (int) $parts['t']) > $tolerance) {
return false;
}
$expected = hash_hmac('sha256', $parts['t'] . '.' . $body, $secret);
return hash_equals($expected, $parts['v1']);
}
Hubz không phải sản phẩm của Zalo hay VNG; Hubz không dùng API chính thức của Zalo. Hubz không liên kết với Odoo S.A.; Odoo là nhãn hiệu của Odoo S.A.