Skip to main content

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.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​

CommandArgumentsWhat it does
opennoneOpens the chat window.
closenoneCloses the chat window.
togglenoneOpens the window if it is closed, closes it if it is open.
hidenoneHides the launcher button. An open window stays open.
shownoneShows 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.
pushEventname, { ...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.
unreadCountcallback(count)Calls callback with the current number of unread replies.
onevent, callbackSubscribes to a widget event. See Events.
offevent, callbackRemoves a callback added with on. Pass the same function.
resetnoneForgets 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 }
}]);
FieldTypeLimit
user_idstringUp to 200 characters. Your own user ID.
emailstringA valid email address, up to 320 characters.
namestringUp to 200 characters.
phonestringUp to 40 characters.
hashstringHMAC-SHA256(identity secret, user_id) in hex, computed on your server.
dataobjectCustom 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.

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​

$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]);
EventDataWhen
ready{ visitor_id, conversation_id }The widget has started. conversation_id is null if the visitor has no open conversation.
opennoneThe chat window opened.
closenoneThe 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​

<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") }
}]);
});
FieldTypeLimit
namestringUp to 200 characters.
phonestringUp to 40 characters. A phone or an email is required.
emailstringA valid email, up to 320 characters.
messagestringUp to 2,000 characters.
sourcestringUp to 100 characters. Shown in the conversation and used as the campaign name in analytics. Default form.
fieldsobjectString 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..." }
StatusErrorCause
403origin_not_allowedThe page's origin is not allowed for the site.
403site_disabledThe site is disabled.
422validation_errorNo phone and no email, an invalid email, or a field over its limit.
429rate_limit_exceededMore 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:

ConversionWhengtag eventdataLayer eventMeta Pixel
chat_startedThe visitor sends the first message of a conversation.chat_startedhatang_chat_startedContact
leadThe visitor submits a campaign form, or the page calls lead.generate_leadhatang_generate_leadLead
purchaseAn order in the chat is paid.purchasehatang_purchasePurchase
  • 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​

View this page as Markdown