# JavaScript API

Widget cung cấp một API nhỏ qua `window.$hatang`. Dùng nó để mở chat từ nút của bạn, cho hộp thư biết khách là ai, thêm ngữ cảnh cho nhân viên, gửi lead từ form của bạn và phản ứng khi có tin nhắn mới.

## Trước khi bắt đầu

[Cài widget](https://hatang.dev/vi/docs/guides/support/web-widget/install.md) bằng đoạn mã hai thẻ. Thẻ thứ nhất tạo `window.$hatang` dưới dạng hàng đợi, nên bạn gọi lệnh được cả trước khi widget tải xong:

```html
<script>window.$hatang=window.$hatang||[];window.HATANG_SITE_ID="site_123...";</script>
<script src="https://chat.hatang.dev/widget.js" async></script>
```

## Cách lệnh hoạt động

Mỗi lệnh là một mảng: tên lệnh, rồi tới các tham số.

```js
window.$hatang = window.$hatang || [];
$hatang.push(["open"]);
```

- Trước khi widget tải xong, lệnh nằm trong hàng đợi và chạy theo thứ tự khi widget khởi động.
- Sau khi tải xong, `$hatang.push` chạy lệnh ngay. Các lệnh cũng có dạng hàm, ví dụ `$hatang.open()`.
- Lệnh không tồn tại bị bỏ qua. Lệnh không bao giờ throw, nên một lỗi nhỏ không làm hỏng trang.

## Lệnh

| Lệnh | Tham số | Tác dụng |
|---|---|---|
| `open` | không | Mở khung chat. |
| `close` | không | Đóng khung chat. |
| `toggle` | không | Mở khung nếu đang đóng, đóng nếu đang mở. |
| `hide` | không | Ẩn nút chat. Khung đang mở vẫn mở. |
| `show` | không | Hiện lại nút chat (kể cả sau `hideLauncher`). |
| `identify` | `{ user_id?, email?, name?, phone?, hash?, data? }` | Cho hộp thư biết khách là ai. Xem [identify](#identify). |
| `setData` | `{ ...thuộc tính }` | Thêm thuộc tính tuỳ chỉnh cho nhân viên xem. Khoá mới gộp vào dữ liệu đã có. |
| `setLocale` | `"vi"` hoặc `"en"` | Đổi ngôn ngữ widget và ghi nhớ trên trình duyệt này. |
| `pushEvent` | `name, { ...data }` | Ghi một sự kiện, ví dụ `"viewed_pricing"`: vào hội thoại đang mở, hoặc vào hành trình của khách trước khi nhắn. Xem [pushEvent](#pushevent). |
| `lead` | `{ name?, phone?, email?, message?, source?, fields? }` | Tạo lead từ form của bạn, không cần mở chat. Xem [Lead từ form của bạn](#leads). |
| `unreadCount` | `callback(count)` | Gọi `callback` với số tin trả lời chưa đọc hiện tại. |
| `on` | `event, callback` | Đăng ký nghe một sự kiện. Xem [Sự kiện](#events). |
| `off` | `event, callback` | Huỷ callback đã đăng ký bằng `on`. Truyền đúng hàm đó. |
| `reset` | không | Quên khách trên trình duyệt này và tải lại trang. Gọi khi người dùng đăng xuất. |

### Mở chat từ phần tử bất kỳ

Thêm `data-hatang-open` vào phần tử bất kỳ. Bấm vào phần tử đó, hoặc vào thứ nằm trong nó, sẽ mở chat. Không cần JavaScript:

```html
<button data-hatang-open>Chat với chúng tôi</button>
<a href="#" data-hatang-open>Cần tư vấn? Nhắn ngay</a>
```

Muốn chỉ dùng nút của mình, ẩn nút chat bằng `window.HATANG_CONFIG = { hideLauncher: true }` và tự hiện số tin chưa đọc qua sự kiện `unread`.

### identify {#identify}

```js
$hatang.push(["identify", {
  user_id: "u_42",
  email: "ha@example.com",
  name: "Nguyễn Thu Hà",
  phone: "0912345678",
  hash: "<HMAC từ server của bạn>",
  data: { plan: "pro", orders: 12 }
}]);
```

| Trường | Kiểu | Giới hạn |
|---|---|---|
| `user_id` | string | Tối đa 200 ký tự. ID người dùng trong hệ thống của bạn. |
| `email` | string | Email hợp lệ, tối đa 320 ký tự. |
| `name` | string | Tối đa 200 ký tự. |
| `phone` | string | Tối đa 40 ký tự. |
| `hash` | string | `HMAC-SHA256(identity secret, user_id)` dạng hex, tính ở server. |
| `data` | object | Thuộc tính tuỳ chỉnh, tối đa 8 KB dạng JSON. |

Không có `hash`, thông tin được lưu là thông tin khách tự khai và hộp thư gắn nhãn **Chưa xác minh**. Có `hash` hợp lệ, hộp thư gắn nhãn **Đã xác minh** và nối mọi phiên của người dùng đó. Đọc [Nhận diện khách](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md) trước khi dùng `user_id`.

> **Caution**
>
> Tên trường là `user_id`, viết kiểu snake case. Các khoá khác bị bỏ đi. Nếu `email` không hợp lệ hoặc một giá trị vượt giới hạn, cả lệnh `identify` bị từ chối, nên hãy kiểm tra dữ liệu trước.

### setData

```js
$hatang.push(["setData", { plan: "pro", cart_value: 450000, last_order: "DH-1042" }]);
```

`setData` là `identify` chỉ có `data`. Nhân viên thấy các thuộc tính trong hồ sơ khách ở hộp thư. Khoá mới được thêm, khoá trùng bị ghi đè, khoá không gửi thì giữ nguyên. Mỗi lần gọi tối đa 8 KB JSON.

### pushEvent

```js
$hatang.push(["pushEvent", "viewed_pricing", { plan: "pro" }]);
```

Trong lúc có hội thoại, sự kiện hiện trong hội thoại đó, để nhân viên biết khách đang làm gì khi chat. Tên tối đa 100 ký tự.

Trước khi khách nhắn, các sự kiện và trang khách xem được giữ làm hành trình của khách: 20 bước gần nhất, trong 24 giờ. Khi khách mở hội thoại (tin nhắn đầu tiên, form offline, lead hay chiến dịch), hành trình được gắn vào hội thoại và hiện cho nhân viên dưới mục **Trước khi nhắn** trong hội thoại và thanh thông tin khách. Khách không bao giờ thấy. Cùng một trang lặp lại liền nhau chỉ tính một lần. Dữ liệu của mỗi sự kiện được cắt còn 1 KB JSON, và giá trị dưới các khoá trông như bí mật (chứa `password`, `token`, `secret`, `card`, `cvv` hoặc `otp`, không phân biệt hoa thường) được lưu thành `[redacted]`, kể cả trong URL trang. Đừng gửi dữ liệu cá nhân mà bạn không muốn nhân viên thấy.

Widget cũng tự báo khi khách chuyển trang, kể cả khi ứng dụng một trang đổi URL (`history.pushState`, `replaceState` và nút quay lại), nên nhân viên thấy trang hiện tại trong **Hỗ trợ → Đang truy cập** và trong hộp thư.

## Sự kiện {#events}

Đăng ký bằng `on`, huỷ bằng `off`:

```js
function onReply(message) {
  console.log("Trả lời mới từ", message.sender_type, message.content);
}
$hatang.push(["on", "message:received", onReply]);
// sau đó
$hatang.push(["off", "message:received", onReply]);
```

| Sự kiện | Dữ liệu | Khi nào |
|---|---|---|
| `ready` | `{ visitor_id, conversation_id }` | Widget đã khởi động. `conversation_id` là `null` nếu khách chưa có hội thoại đang mở. |
| `open` | không | Khung chat mở. |
| `close` | không | Khung chat đóng. |
| `message:received` | `{ id, sender_type, content }` | Nhân viên, trợ lý AI hoặc hệ thống nhắn trong hội thoại của khách. `sender_type` là `agent`, `bot` hoặc `system`. |
| `message:sent` | `{ id, sender_type, content }` | Tin của khách đã được lưu, kể cả tin gửi từ tab khác. `sender_type` là `visitor`. |
| `conversation:status` | `{ status, conversation_id }` | Hội thoại đổi trạng thái: `open`, `pending`, `resolved` hoặc `closed`. |
| `unread` | `{ count }` | Số tin trả lời chưa đọc thay đổi. Về 0 khi khách mở khung chat. |
| `conversion` | `{ name, ...data }` | Một chuyển đổi được ghi nhận: `name` là `chat_started`, `lead` hoặc `purchase`. Chỉ phát khi bật theo dõi chuyển đổi. |

> **Tip**
>
> `ready` chỉ phát một lần, ngay sau khi các lệnh trong hàng đợi chạy xong. Đặt `on("ready", ...)` trước khi script widget tải, ví dụ trong thẻ thứ nhất của đoạn mã, nếu không sẽ lỡ sự kiện này.

### Ví dụ: huy hiệu chưa đọc của riêng bạn

```html
<button id="help" data-hatang-open>Hỗ trợ <span id="help-count"></span></button>
<script>
  window.$hatang = window.$hatang || [];
  window.HATANG_CONFIG = { hideLauncher: true };
  $hatang.push(["on", "unread", function (e) {
    document.getElementById("help-count").textContent = e.count ? "(" + e.count + ")" : "";
  }]);
</script>
```

## Lead từ form của bạn {#leads}

Bất kỳ form nào trên website cũng tạo được lead. Widget nhận diện khách bằng thông tin trong form, mở hội thoại (hoặc dùng hội thoại đang mở của khách) với một tin tóm tắt form, phân phối theo quy tắc phân phối của bạn và tính một chuyển đổi `lead`.

```js
document.querySelector("#quote-form").addEventListener("submit", function (e) {
  e.preventDefault();
  const f = new FormData(e.target);
  $hatang.push(["lead", {
    name: f.get("name"),
    phone: f.get("phone"),
    email: f.get("email") || undefined,
    message: f.get("message"),
    source: "pricing-form",
    fields: { city: f.get("city"), budget: f.get("budget") }
  }]);
});
```

| Trường | Kiểu | Giới hạn |
|---|---|---|
| `name` | string | Tối đa 200 ký tự. |
| `phone` | string | Tối đa 40 ký tự. Cần số điện thoại hoặc email. |
| `email` | string | Email hợp lệ, tối đa 320 ký tự. |
| `message` | string | Tối đa 2.000 ký tự. |
| `source` | string | Tối đa 100 ký tự. Hiện trong hội thoại và dùng làm tên chiến dịch trong hệ thống phân tích. Mặc định `form`. |
| `fields` | object | Chỉ giá trị chuỗi: khoá tối đa 60 ký tự, giá trị tối đa 500. Lưu vào khách truy cập và hồ sơ khách hàng. |

Mỗi lead cũng phát sự kiện [webhook](https://hatang.dev/vi/docs/guides/getting-started/webhooks.md) `chat.campaign.lead`.

### Không cần widget

Landing page tĩnh có thể gửi cùng JSON thẳng tới chat gateway. Request phải đến từ một trang trên trình duyệt thuộc origin được phép của site; request không có header `Origin` bị từ chối.

```js
await fetch("https://chat.hatang.dev/widget/v1/sites/site_123.../leads", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ name: "Hà", phone: "0912345678", message: "Báo giá gói Pro", source: "landing-tet" })
});
```

Gọi thành công trả về `201`:

```json
{ "ok": true, "conversation_id": "cnv_123...", "visitor_id": "vis_123...", "customer_id": "cus_123..." }
```

| Mã | Lỗi | Nguyên nhân |
|---|---|---|
| 403 | `origin_not_allowed` | Origin của trang không được phép với site. |
| 403 | `site_disabled` | Site đang tắt. |
| 422 | `validation_error` | Không có số điện thoại lẫn email, email sai, hoặc một trường vượt giới hạn. |
| 429 | `rate_limit_exceeded` | Quá 10 lead mỗi phút từ một địa chỉ IP. |

Thêm một ô ẩn tên `website` vào form và gửi kèm giá trị của nó: đó là honeypot. Người thật để trống; nếu bot điền vào, gateway trả `200 { "ok": true }` và không lưu gì.

## Theo dõi chuyển đổi {#conversion-tracking}

Bật ở **Hỗ trợ → Kênh → website của bạn → Tự động hoá → Theo dõi chuyển đổi**. Khi đó widget báo ba loại chuyển đổi:

| Chuyển đổi | Khi nào | Sự kiện gtag | Sự kiện dataLayer | Meta Pixel |
|---|---|---|---|---|
| `chat_started` | Khách gửi tin đầu tiên của một hội thoại. | `chat_started` | `hatang_chat_started` | `Contact` |
| `lead` | Khách gửi form của chiến dịch, hoặc trang gọi `lead`. | `generate_lead` | `hatang_generate_lead` | `Lead` |
| `purchase` | Một đơn hàng trong chat được thanh toán. | `purchase` | `hatang_purchase` | `Purchase` |

- Nếu trang có `gtag`, widget gọi `gtag("event", ...)` kèm `source: "hatang_chat"`. Nếu bạn điền **Chuyển đổi Google Ads (send_to)**, widget gửi thêm sự kiện `conversion` tới đích đó.
- Nếu không có `gtag` nhưng có `window.dataLayer`, widget đẩy sự kiện dataLayer cho Google Tag Manager.
- Nếu trang có `fbq`, widget ghi sự kiện Meta Pixel.
- Trong mọi trường hợp, widget phát sự kiện `conversion`.

Điền **GA4 measurement id**, **GA4 API secret**, **Meta pixel id** và **Meta Conversions API token** để gửi thêm sự kiện phía server (GA4 Measurement Protocol, Meta Conversions API). Các sự kiện này không bị trình chặn quảng cáo chặn và tính cả đơn thanh toán từ Zalo, Messenger, Instagram. Khi khởi động, widget gửi các cookie `_ga`, `_fbp`, `_fbc` để hai phía nối vào cùng một phiên phân tích.

## Bước tiếp theo

- [Nhận diện khách](https://hatang.dev/vi/docs/guides/support/web-widget/identity-verification.md): ký `identify` để hộp thư tin `user_id`.
- [SDK React](https://hatang.dev/vi/docs/guides/sdks/react.md): `<HatangChat />` và `useHatang()` bọc API này.
- [Realtime](https://hatang.dev/vi/docs/guides/support/realtime.md) và [giao thức realtime](https://hatang.dev/vi/docs/reference/realtime-protocol.md): tự viết client riêng.

Source: https://hatang.dev/vi/docs/guides/support/web-widget/javascript-api
