# JavaScript API

The widget exposes a small API on `window.$hatang`. Use it to open the chat from your own buttons, tell the inbox who the visitor is, add context for your agents, send leads from your own forms and react to new messages.

## Before you start

[Install the widget](https://hatang.dev/docs/guides/support/web-widget/install.md) with the two-tag snippet. The first tag creates `window.$hatang` as a queue, so you can push commands before the widget has loaded:

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

## How commands work

Every command is an array: the command name, then its arguments.

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

- Before the widget loads, commands wait in the queue and run in order once it starts.
- After it loads, `$hatang.push` runs the command at once. The commands are also available as methods, for example `$hatang.open()`.
- Unknown commands are ignored. Commands never throw, so a mistake does not break your page.

## Commands

| Command | Arguments | What it does |
|---|---|---|
| `open` | none | Opens the chat window. |
| `close` | none | Closes the chat window. |
| `toggle` | none | Opens the window if it is closed, closes it if it is open. |
| `hide` | none | Hides the launcher button. An open window stays open. |
| `show` | none | Shows the launcher button again (also after `hideLauncher`). |
| `identify` | `{ user_id?, email?, name?, phone?, hash?, data? }` | Tells the inbox who the visitor is. See [identify](#identify). |
| `setData` | `{ ...attributes }` | Adds custom attributes shown to agents. Keys merge into what is already stored. |
| `setLocale` | `"vi"` or `"en"` | Switches the widget language and remembers it in this browser. |
| `pushEvent` | `name, { ...data }` | Records an event, for example `"viewed_pricing"`: on the active conversation, or in the visitor's journey before they write. See [pushEvent](#pushevent). |
| `lead` | `{ name?, phone?, email?, message?, source?, fields? }` | Creates a lead from your own form without opening the chat. See [Leads from your own forms](#leads). |
| `unreadCount` | `callback(count)` | Calls `callback` with the current number of unread replies. |
| `on` | `event, callback` | Subscribes to a widget event. See [Events](#events). |
| `off` | `event, callback` | Removes a callback added with `on`. Pass the same function. |
| `reset` | none | Forgets the visitor in this browser and reloads the page. Call it when a user signs out. |

### Open the chat from any element

Add `data-hatang-open` to any element. A click on it, or on anything inside it, opens the chat. No JavaScript needed:

```html
<button data-hatang-open>Chat with us</button>
<a href="#" data-hatang-open>Questions? Ask us</a>
```

To use only your own button, hide the launcher with `window.HATANG_CONFIG = { hideLauncher: true }` and show the unread count yourself with the `unread` event.

### identify {#identify}

```js
$hatang.push(["identify", {
  user_id: "u_42",
  email: "ha@example.com",
  name: "Nguyen Thu Ha",
  phone: "0912345678",
  hash: "<HMAC from your server>",
  data: { plan: "pro", orders: 12 }
}]);
```

| Field | Type | Limit |
|---|---|---|
| `user_id` | string | Up to 200 characters. Your own user ID. |
| `email` | string | A valid email address, up to 320 characters. |
| `name` | string | Up to 200 characters. |
| `phone` | string | Up to 40 characters. |
| `hash` | string | `HMAC-SHA256(identity secret, user_id)` in hex, computed on your server. |
| `data` | object | Custom attributes, up to 8 KB as JSON. |

Without `hash`, the details are stored as the visitor's own claim and the inbox marks the visitor **Unverified**. With a valid `hash`, the inbox marks the visitor **Verified** and links all their sessions. Read [Identity verification](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md) before you use `user_id`.

> **Caution**
>
> The field name is `user_id`, in snake case. Other keys are dropped. If `email` is not a valid address, or a value is over its limit, the whole `identify` is rejected, so validate on your side first.

### setData

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

`setData` is `identify` with only `data`. Agents see the attributes on the visitor's profile in the inbox. New keys are added and existing keys are overwritten; keys you do not send are kept. Each call can carry up to 8 KB of JSON.

### pushEvent

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

During a conversation, the event appears in it, so agents see what the visitor does while they chat. The name can be up to 100 characters.

Before the visitor writes, events and the pages they visit are kept as their journey: the last 20 steps, for 24 hours. When the visitor starts a conversation (a first message, the offline form, a lead or a campaign), the journey is attached to it and shown to agents as **Before writing** in the thread and the visitor sidebar. Visitors never see it. Repeats of the same page in a row count once. Each event's data is cut to 1 KB of JSON, and values under keys that look secret (anything containing `password`, `token`, `secret`, `card`, `cvv` or `otp`, in any case) are stored as `[redacted]`, also in page URLs. Do not send personal data you would not show an agent.

The widget also reports page changes on its own, including URL changes in single-page apps (`history.pushState`, `replaceState` and the back button), so agents see the current page in **Support → Live** and the inbox.

## Events {#events}

Subscribe with `on`, unsubscribe with `off`:

```js
function onReply(message) {
  console.log("New reply from", message.sender_type, message.content);
}
$hatang.push(["on", "message:received", onReply]);
// later
$hatang.push(["off", "message:received", onReply]);
```

| Event | Data | When |
|---|---|---|
| `ready` | `{ visitor_id, conversation_id }` | The widget has started. `conversation_id` is `null` if the visitor has no open conversation. |
| `open` | none | The chat window opened. |
| `close` | none | The chat window closed. |
| `message:received` | `{ id, sender_type, content }` | An agent, the AI assistant or the system wrote in the visitor's conversation. `sender_type` is `agent`, `bot` or `system`. |
| `message:sent` | `{ id, sender_type, content }` | A visitor message was stored, including one sent from another tab. `sender_type` is `visitor`. |
| `conversation:status` | `{ status, conversation_id }` | The conversation changed status: `open`, `pending`, `resolved` or `closed`. |
| `unread` | `{ count }` | The number of unread replies changed. It is reset to 0 when the visitor opens the chat. |
| `conversion` | `{ name, ...data }` | A conversion was tracked: `name` is `chat_started`, `lead` or `purchase`. Fires only when conversion tracking is on. |

> **Tip**
>
> `ready` fires once, right after the queued commands run. Push your `on("ready", ...)` before the widget script loads, as part of the first snippet tag, or you will miss it.

### Example: your own unread badge

```html
<button id="help" data-hatang-open>Help <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>
```

## Leads from your own forms {#leads}

Any form on your site can create a lead. The widget identifies the visitor with the details, opens a conversation (or reuses the visitor's active one) whose message summarizes the form, routes it by your distribution rules and counts a `lead` conversion.

```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") }
  }]);
});
```

| Field | Type | Limit |
|---|---|---|
| `name` | string | Up to 200 characters. |
| `phone` | string | Up to 40 characters. A phone or an email is required. |
| `email` | string | A valid email, up to 320 characters. |
| `message` | string | Up to 2,000 characters. |
| `source` | string | Up to 100 characters. Shown in the conversation and used as the campaign name in analytics. Default `form`. |
| `fields` | object | String values only: keys up to 60 characters, values up to 500. Stored on the visitor and on the customer profile. |

Each lead also sends the `chat.campaign.lead` [webhook](https://hatang.dev/docs/guides/getting-started/webhooks.md) event.

### Without the widget

A static landing page can post the same JSON straight to the chat gateway. The request must come from a browser page on an allowed origin of the site; calls without an `Origin` header are rejected.

```js
await fetch("https://chat.hatang.dev/widget/v1/sites/site_123.../leads", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ name: "Ha", phone: "0912345678", message: "Pro plan quote", source: "landing-tet" })
});
```

A successful call returns `201`:

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

| Status | Error | Cause |
|---|---|---|
| 403 | `origin_not_allowed` | The page's origin is not allowed for the site. |
| 403 | `site_disabled` | The site is disabled. |
| 422 | `validation_error` | No phone and no email, an invalid email, or a field over its limit. |
| 429 | `rate_limit_exceeded` | More than 10 leads per minute from one IP address. |

Add a hidden `website` input to your form and send its value: it is a honeypot. Real people leave it empty; if a bot fills it, the gateway answers `200 { "ok": true }` and stores nothing.

## Conversion tracking {#conversion-tracking}

Turn it on under **Support → Channels → your website → Automation → Conversion tracking**. The widget then reports three conversions:

| Conversion | When | gtag event | dataLayer event | Meta Pixel |
|---|---|---|---|---|
| `chat_started` | The visitor sends the first message of a conversation. | `chat_started` | `hatang_chat_started` | `Contact` |
| `lead` | The visitor submits a campaign form, or the page calls `lead`. | `generate_lead` | `hatang_generate_lead` | `Lead` |
| `purchase` | An order in the chat is paid. | `purchase` | `hatang_purchase` | `Purchase` |

- If `gtag` is on the page, the widget calls `gtag("event", ...)` with `source: "hatang_chat"`. If you set **Google Ads conversion (send_to)**, it also sends a `conversion` event to that target.
- If there is no `gtag` but `window.dataLayer` exists, it pushes the dataLayer event for Google Tag Manager.
- If `fbq` is on the page, it tracks the Meta Pixel event.
- In every case, it emits the `conversion` widget event.

Fill **GA4 measurement id**, **GA4 API secret**, **Meta pixel id** and **Meta Conversions API token** to also send server-side events (GA4 Measurement Protocol, Meta Conversions API). These are not blocked by ad blockers and also count orders paid from Zalo, Messenger and Instagram. When it starts, the widget sends the `_ga`, `_fbp` and `_fbc` cookies so both sides join the same analytics session.

## Next steps

- [Identity verification](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md): sign `identify` so the inbox can trust `user_id`.
- [React SDK](https://hatang.dev/docs/guides/sdks/react.md): `<HatangChat />` and `useHatang()` wrap this API.
- [Realtime](https://hatang.dev/docs/guides/support/realtime.md) and the [realtime protocol](https://hatang.dev/docs/reference/realtime-protocol.md): build your own client.

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