Bỏ qua để đến Nội dung
Beta: Hubz API v1 đang được triển khai lên production. Endpoint và giới hạn có thể thay đổi; mọi thay đổi được ghi ở Nhật ký thay đổi.

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ệnKhi nào
message.createdCó 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.updatedMột tin thay đổi, ví dụ trạng thái gửi.
conversation.updatedMộ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.