# Chat gateway endpoints

Public endpoints used by the chat widget and the native SDKs. Every /widget/v1 route checks the request Origin against the site's domain and allowed origins, unless the caller presents a site app key (x-hatang-app-key) or a visitor token minted through one; authenticated calls carry the visitor token from bootstrap. Realtime traffic uses the websocket at /ws?site=…(&app=…&install=… for apps), protocol v1; close codes: 4001 auth_timeout, 4003 auth_failed, 4004 protocol_outdated, 4005 session_replaced. See docs/chat-protocol.md and docs/chat-native-sdk.md.

Base URL: https://chat.hatang.dev

## Widget

- `POST /widget/v1/sites/{siteId}/bootstrap`: Create or resume a visitor session (browsers: allowed Origin; native SDKs: app key + `app.install_id`)
- `POST /widget/v1/sites/{siteId}/conversations`: Start a conversation (HTTP fallback)
- `GET /widget/v1/sites/{siteId}/conversations`: The visitor's own conversations, newest activity first, resolved ones included (an app's inbox)
- `GET /widget/v1/sites/{siteId}/conversations/{id}/messages`: Timeline items of one of the visitor's conversations: after `after_seq` (sync) or before `before_seq` (earlier history); `hasMore` is kept one release for old bundles
- `POST /widget/v1/sites/{siteId}/conversations/{id}/messages`: Send a message (HTTP fallback); content may be empty when attachments are given
- `POST /widget/v1/sites/{siteId}/leads`: Capture a lead from any form: creates/identifies the visitor, opens a conversation, routes it and counts a conversion (no visitor token needed; 10/min per IP)
- `POST /widget/v1/sites/{siteId}/offline-message`: Pre-chat form: name, email and first message while nobody is online
- `POST /widget/v1/sites/{siteId}/uploads`: Upload an attachment (multipart, field `file`, 10 MB; images incl. HEIC, PDF, office files, zip, mp4, m4a)
- `POST /widget/v1/sites/{siteId}/purchases`: Report a finished App Store / Google Play purchase from the app (SDK reportPurchase). With `signed_transaction` (StoreKit 2 JWS) or `purchase_token` (Play) it is checked with the store and recorded for this visitor (Play purchases are acknowledged); a store that says no is a 422. The team also gets an internal note in the open conversation
- `PUT /widget/v1/sites/{siteId}/devices`: Native SDKs: register this phone's push token for the visitor (FCM v1 with the site's Firebase credentials); re-registering moves the token to the current visitor
- `DELETE /widget/v1/sites/{siteId}/devices`: Forget this phone's push token
- `POST /widget/v1/sites/{siteId}/reset`: Forget this visitor: the old token stops working and a fresh visitor is issued
- `POST /widget/v1/sites/{siteId}/sessions/restore`: Exchange a magic-link token from a chat email for a visitor token (with an app key, the token is minted for that app)

## Files

- `GET /files/{id}`: Download an attachment with a signed URL
- `POST /agent/uploads`: Upload an attachment as an agent (Bearer agent token, multipart `file` + `site_id`; 10 MB, videos 25 MB, a .mov is stored as MP4 when the host has ffmpeg)

## Agent

- `GET /app/version`: Support app update check (public): `update` is none, available (below APP_<PLATFORM>_LATEST) or required (below APP_<PLATFORM>_MIN). Apps send `X-Hatang-App: <platform>/<version>` on /agent/* requests; a build below the minimum gets 426 upgrade_required
- `PUT /agent/devices`: Register the support app on this phone or Mac (Bearer mobile agent token); upserts by device_id. Phones send `expo_push_token`; the Mac app (platform macos) sends `apns_token` and `apns_env`
- `DELETE /agent/devices/{deviceId}`: Forget this phone (sign-out)
- `GET /agent/devices/{deviceId}/prefs`: This phone's notification preferences
- `PATCH /agent/devices/{deviceId}/prefs`: Change mute, event kinds, quiet hours or time zone for this phone
- `GET /agent/canned`: Canned responses: the agent's own (private) ones first, then the team's
- `POST /agent/canned`: Add a canned response: the agent's own, or the team's with `shared` (chat:manage). An empty shortcut is made from the title
- `GET /agent/conversations/{id}/scheduled`: The conversation's scheduled messages (waiting or failed), soonest first
- `POST /agent/conversations/{id}/scheduled`: Schedule a reply: sent at `send_at` as this agent's message. At least a minute ahead, within 30 days and before the channel's reply window closes (chat:reply)
- `PATCH /agent/scheduled/{id}`: Change the text or the time of a scheduled (or failed) message
- `DELETE /agent/scheduled/{id}`: Cancel a scheduled message
- `POST /agent/scheduled/{id}/send`: Send a scheduled message now
- `PATCH /agent/canned/{id}`: Edit one of the agent's own responses (the team's need chat:manage); fields are optional
- `DELETE /agent/canned/{id}`: Delete one of the agent's own responses (the team's need chat:manage)
- `GET /agent/profile-links`: The business's profile links: URL templates (https, {{user_id}} = verified identify() id else the claimed one marked unverified, {{email}}, {{phone}}, {{name}}, {{customer_id}}, {{conversation_id}}, {{site_id}}, {{field.<key>}}, {{data.<key>}}) the client fills per conversation; links with a missing value are hidden
- `GET /agent/home`: Home summary for the signed-in employee: chat, orders, tickets, payments and employees sections by role and switched-on module, plus an attention list
- `GET /agent/reminders`: The signed-in agent's open reminders (create and complete them over the socket)
- `GET /agent/agents`: Employees who can pick up conversations (assign picker)
- `GET /agent/conversations`: Inbox snapshot: `status` open (default; includes pending) | pending | resolved | closed | all, `scope` mine | unassigned | all, `q`, `after` cursor, `unanswered=1`, `flag`, `limit` ≤ 100
- `GET /agent/conversations/{id}`: One conversation with the latest 100 timeline items (messages carry signed attachment links)
- `GET /agent/conversations/{id}/email`: Email side of a conversation: subject, CC participants, first-answer deadline and how each emailed reply landed (null when the conversation never used email)
- `POST /agent/conversations/{id}/rating-request`: Ask the customer for a rating now (whether or not automatic CSAT is on); result "rated" when they already did
- `POST /agent/conversations/{id}/block`: Block the conversation's customer: this and their later conversations go to Spam
- `POST /agent/conversations/{id}/mute`: Mute (body { muted: true }) or unmute the conversation for the signed-in agent
- `POST /agent/conversations/{id}/ai`: Pause (body { paused: true }) or resume the AI assistant in the conversation
- `POST /agent/conversations/{id}/learn`: Teach the AI from one of our public replies: the AI reads the exchange, the closest knowledge and the AI instructions and drafts a Q/A and/or one instruction line (nothing is saved). Needs chat:reply; 429 ai_budget_exhausted, 503 ai_not_configured / ai_unavailable
- `POST /agent/conversations/{id}/learn/save`: Save the confirmed draft: the Q/A joins the "Câu hỏi từ khách" FAQ (skipped when the same question is there; a matching open knowledge gap is answered), the instruction is appended as "- …" (409 instructions_full past 4000 characters). The reply is then re-sent as a message frame with metadata.learned
- `POST /agent/messages/{id}/recall`: Recall a message we sent (web chat; personal Zalo on Zalo too); the customer keeps a recalled marker, the team a private note
- `GET /agent/customers/{id}`: The customer card: record, latest notes and the business's custom fields (CRM on and readable)
- `PATCH /agent/customers/{id}`: Edit any of name, email, phone, owner_id, stage, labels, fields
- `POST /agent/customers/{id}/notes`: Add a note about the customer (body { body })
- `DELETE /agent/customers/{id}/notes/{noteId}`: Delete one of your own notes
- `POST /agent/ai/drafts/{id}`: Insert or discard the AI assistant's suggested reply

## Hosted

- `GET /c/{token}`: Hosted continuation page opened from a chat email; loads the widget full-page and restores the session
- `GET /rate/{id}`: One-click CSAT from an email: records the score (s=1..5, signed link) and offers a comment form
- `POST /rate/{id}`: Comment for an email rating (form post from the rating page)

## Assets

- `GET /widget.js`: Widget loader (mutable, 5 min cache)
- `GET /w/{file}`: Hashed widget bundle (immutable)
- `GET /preview`: Live widget preview for the dashboard's site settings: the real bundle with a stub boot and no network, configured by postMessage from the dashboard (the only page allowed to frame it)
