# 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](https://hatang.dev/docs/reference/realtime-protocol.md) and the HTTP endpoints in the [chat gateway reference](https://hatang.dev/docs/reference/chat-gateway.md).

## Pick the right interface

| You want to | Use |
|---|---|
| Put chat on a website | The [web widget](https://hatang.dev/docs/guides/support/web-widget/install.md); it speaks the protocol for you |
| Put chat in a mobile app | A native SDK ([Mobile apps](https://hatang.dev/docs/guides/support/mobile-apps.md)) |
| 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](https://hatang.dev/docs/guides/support/rest-api.md) with an API key, plus [webhooks](https://hatang.dev/docs/guides/getting-started/webhooks.md) |

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 gets `403 origin_not_allowed`.
- **Native apps** send their app key in the `x-hatang-app-key` header and an `app.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.

```js
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:

```json
{ "t": "message.send", "id": "req_1", "v": 1, "d": { "client_id": "c_9f2", "content": "Shop còn hàng không ạ?" } }
```

1. Show the message as pending and wait up to 10 seconds for `message.ack`, which returns the message `id`, its `seq` and the `conversation_id`. The first message of a visitor creates the conversation.
2. 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.
3. The message itself also comes back as a `message` frame. Deduplicate by `id`.

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](https://hatang.dev/docs/guides/support/web-widget/identity-verification.md)). 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`, `typing` and presence frames, and send messages, notes, assignments, status changes, tags and reminders.
- **HTTP.** The apps also call `/agent/*` endpoints, for example `GET /agent/conversations` for an inbox snapshot, `POST /agent/uploads` for attachments (10 MB, videos 25 MB; a .mov is stored as MP4), `POST/PATCH/DELETE /agent/canned` for the agent's own canned responses, and `/agent/conversations/{id}/scheduled` plus `/agent/scheduled/{id}` for scheduled messages (their changes arrive to every agent as the `scheduled` frame). 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](https://hatang.dev/help/account/employees-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](https://hatang.dev/docs/guides/support/rest-api.md) with an API key and subscribe to [webhooks](https://hatang.dev/docs/guides/getting-started/webhooks.md).

## 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](https://hatang.dev/docs/reference/realtime-protocol.md): every frame, field and limit
- [Chat gateway reference](https://hatang.dev/docs/reference/chat-gateway.md): the widget, SDK and agent HTTP endpoints
- [JavaScript API](https://hatang.dev/docs/guides/support/web-widget/javascript-api.md): drive the widget instead of writing a client
- [Support REST API](https://hatang.dev/docs/guides/support/rest-api.md)

Source: https://hatang.dev/docs/guides/support/realtime
