Realtime
Every chat client talks to the chat gateway at https://chat.hatang.dev over one WebSocket protocol. This page explains how a session works from start to finish, so you can debug the widget, understand what the SDKs do, or write your own client. The frame-by-frame details are in the realtime protocol reference and the HTTP endpoints in the chat gateway reference.
Pick the right interface
| You want to | Use |
|---|---|
| Put chat on a website | The web widget; it speaks the protocol for you |
| Put chat in a mobile app | A native SDK (Mobile apps) |
| Build your own customer-facing chat UI | The visitor protocol described below |
| Read and answer conversations from your server, bot or CRM | The Support REST API with an API key, plus webhooks |
The agent side of the gateway serves Ha Tang's own dashboard and apps for iPhone, Android and Mac. It needs an employee's sign-in, not an API key, so server integrations should use the REST API instead.
The visitor session
A visitor is one browser or one app install on one site. The session runs in five steps.
1. Bootstrap
The client calls POST /widget/v1/sites/{siteId}/bootstrap once when it starts.
- Browsers are checked by
Origin. The page must be on the site's domain, a subdomain of it, or one of the site's extra allowed origins. Anything else gets403 origin_not_allowed. - Native apps send their app key in the
x-hatang-app-keyheader and anapp.install_id(a stable id per install) in the body. No origin is needed.
The body can carry the stored token (to resume the same visitor), page or screen, and locale. The response contains:
| Field | What it is |
|---|---|
visitor_token | The visitor's credential. Store it (a cookie or local storage on the web, the keychain in an app) and send it back next time |
visitor | The visitor record |
config | The site's widget settings, campaigns and contact buttons |
availability | Whether the team is online or within office hours |
conversation, timeline | The active conversation, if any, and its last 100 visible items |
ws_url | The socket address to open, for example wss://chat.hatang.dev/ws?site=site_... |
protocol_version | Currently 1 |
Other HTTP calls in the session send the token in the x-visitor-token header.
2. Connect and authenticate
Open ws_url exactly as returned and send an auth frame within 5 seconds. Every frame is JSON with a type t, data d, version v and an optional id that the server echoes in its answer.
const ws = new WebSocket(boot.ws_url);
ws.onopen = () => ws.send(JSON.stringify({
t: "auth", v: 1,
d: { token: boot.visitor_token, site_id: "site_...", page: { url: location.href, title: document.title } },
}));
ws.onmessage = (e) => {
const frame = JSON.parse(e.data);
if (frame.t === "ready") render(frame.d.conversation, frame.d.timeline);
if (frame.t === "message") append(frame.d);
};
The server answers with ready, which carries the visitor, the active conversation, its timeline and the team's availability. After that, both sides send ping every 25 seconds.
3. Send messages
Each outgoing message gets a client_id that the client generates:
{ "t": "message.send", "id": "req_1", "v": 1, "d": { "client_id": "c_9f2", "content": "Shop còn hàng không ạ?" } }
- Show the message as pending and wait up to 10 seconds for
message.ack, which returns the messageid, itsseqand theconversation_id. The first message of a visitor creates the conversation. - On a timeout or an error, keep the bubble marked as failed and retry with the same
client_id. The server recognizes it and acknowledges the original message, so a retry never posts twice. - The message itself also comes back as a
messageframe. Deduplicate byid.
Visitors can send 5 messages per 10 seconds and 60 per minute. Files are uploaded first with POST /widget/v1/sites/{siteId}/uploads (up to 10 MB), then sent by id in attachments.
4. Receive
While connected, the visitor receives:
| Frame | When |
|---|---|
message | An agent, the AI assistant or the system posts in one of the visitor's conversations |
typing | An agent is typing |
read | The agent side has read up to a seq |
availability | The team goes online or offline |
conversation.update | A conversation's status, unread count or handling agent changes |
event | Events the visitor should see, such as a status change or a handoff to a person |
session | The visitor was merged into another one by a signed identify: store the new token |
The client reports what the visitor does with typing, read, page (browsers) or screen (apps), and identify when the visitor signs in (Identity verification). Apps also send app.state when they go to the background, so replies reach the person as push notifications instead.
5. Reconnect without losing anything
Every message and event in a conversation has a seq that only grows. Keep the highest seq you have seen. After a reconnect, send resume {conversation_id, after_seq} and the server replays everything you missed from the database. Earlier history is paged over HTTP with before_seq.
When the socket closes, look at the close code:
| Code | Meaning | What to do |
|---|---|---|
| 4001 | No auth within 5 seconds | Reconnect and send auth first |
| 4003 | Authentication refused or revoked | Stop; drop the token and bootstrap a new visitor |
| 4004 | The client's protocol is too old | Browsers reload; apps ask the user to update |
| 4005 | A session frame carried a new token | Reconnect at once with the new token |
| Other | Network or gateway restart | Reconnect with backoff, min(30 s, 500 ms · 2^n) plus jitter |
The agent side
The dashboard and the Ha Tang apps use the same gateway as employees:
- Socket. An agent connection authenticates with a short-lived agent token. The dashboard gets one from the signed-in session and it is accepted only from the dashboard's origin; the mobile and Mac apps get theirs when the employee signs in. Agents then subscribe to conversations, receive
message,conversation.upsert,typingand presence frames, and send messages, notes, assignments, status changes, tags and reminders. - HTTP. The apps also call
/agent/*endpoints, for exampleGET /agent/conversationsfor an inbox snapshot,POST /agent/uploadsfor attachments (10 MB, videos 25 MB; a .mov is stored as MP4),POST/PATCH/DELETE /agent/cannedfor the agent's own canned responses, and/agent/conversations/{id}/scheduledplus/agent/scheduled/{id}for scheduled messages (their changes arrive to every agent as thescheduledframe). These are limited to 300 reads and 60 writes per minute per person. - Permissions. The token carries the employee's role. People who can only view Support receive the live inbox but cannot act in it (Team and roles).
Because these tokens belong to a person and expire within an hour, they are not meant for integrations. To automate the inbox, call the Support REST API with an API key and subscribe to webhooks.
Troubleshooting
| Symptom | Likely cause |
|---|---|
origin_not_allowed on bootstrap | The page's domain is not the site's domain, a subdomain of it, or an allowed origin. Add it under Extra allowed origins on the website's page in Support → Channels |
invalid_visitor_token | The token expired, was reset, or belongs to another site. Bootstrap again without it |
product_disabled | The Support module is off for the business |
| The socket closes with 4001 | The client sent something before auth, or took longer than 5 seconds |
| Messages appear twice | The client does not deduplicate the message echo by id |
| Gaps after a network drop | The client does not send resume with its last seq |
Next steps
- Realtime protocol reference: every frame, field and limit
- Chat gateway reference: the widget, SDK and agent HTTP endpoints
- JavaScript API: drive the widget instead of writing a client
- Support REST API