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 with the two-tag snippet. The first tag creates window.$hatang as a queue, so you can push commands before the widget has loaded:
<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.
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.pushruns 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. |
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. |
lead | { name?, phone?, email?, message?, source?, fields? } | Creates a lead from your own form without opening the chat. See Leads from your own forms. |
unreadCount | callback(count) | Calls callback with the current number of unread replies. |
on | event, callback | Subscribes to a widget event. See 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:
<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
$hatang.push(["identify", {
user_id: "u_42",
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 before you use user_id.
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
$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
$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
Subscribe with on, unsubscribe with off:
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. |
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
<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
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.
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 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.
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:
{ "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
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
gtagis on the page, the widget callsgtag("event", ...)withsource: "hatang_chat". If you set Google Ads conversion (send_to), it also sends aconversionevent to that target. - If there is no
gtagbutwindow.dataLayerexists, it pushes the dataLayer event for Google Tag Manager. - If
fbqis on the page, it tracks the Meta Pixel event. - In every case, it emits the
conversionwidget 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: sign
identifyso the inbox can trustuser_id. - React SDK:
<HatangChat />anduseHatang()wrap this API. - Realtime and the realtime protocol: build your own client.