Skip to main content

Support REST API

The Support REST API lets your server work the inbox like an agent: list conversations from every channel, read their timeline, reply or leave a private note, assign, tag and resolve. Use it to connect the inbox to your ERP, CRM, order system or your own bot. Webhooks tell you when something happens, so you do not have to poll.

Before you start​

  • An API key from the dashboard (API Keys). Send it as Authorization: Bearer ht_... to https://api.hatang.dev. See Authentication.

  • The key needs the right scopes:

    ScopeLets the key
    chat:readList and read sites, conversations, timelines, visitors, tickets, reports, flows, workflows, campaigns, canned responses, distribution rules and knowledge sources
    chat:writeEverything chat:read does, plus reply, update conversations, tickets, sites, flows, workflows, campaigns, canned responses, distribution rules and knowledge sources
    crm:read / crm:writeCustomers, custom customer fields and Zalo ZNS (keys with chat:* scopes also work here)
    products:read / products:writeProducts and orders (keys with chat:* scopes also work here)

    A write scope includes read; chat:* and * cover everything.

  • The Support module must be on for the business. While it is off, every endpoint answers 403 product_disabled (Modules).

note

chat:reply and chat:manage are permissions of employees in the dashboard (Team and roles), not API key scopes.

1. List conversations​

GET /chat/conversations returns the newest activity first, 50 per page by default (up to 100).

Query parameterMeaning
statusComma-separated: open, pending, resolved, closed
unansweredtrue: only conversations where the customer sent the last message
assignee_idConversations assigned to this employee
site_idConversations of one site
visitor_idConversations of one visitor
qSearch text (up to 200 characters)
flaghidden (snoozed), spam, or none. By default only unflagged conversations are listed
limit1–100, default 50
afterCursor: the id of the last conversation of the previous page
curl "https://api.hatang.dev/chat/conversations?status=open,pending&unanswered=true&limit=20" \
-H "Authorization: Bearer ht_123..."
Response (shortened)
{
"object": "list",
"has_more": true,
"data": [
{
"id": "cnv_...",
"site_id": "site_...",
"visitor_id": "vis_...",
"channel": "zalo",
"channel_account_id": "...",
"status": "open",
"assignee_kind": null,
"assignee_id": null,
"seq": 14,
"agent_unread": 2,
"visitor_unread": 0,
"last_message_at": "2026-10-23T08:15:02.000Z",
"last_message_preview": "Shop còn size M không ạ?",
"last_message_sender_type": "visitor",
"first_response_at": null,
"tags": [],
"member_ids": [],
"flag": null,
"subject": null,
"created_at": "2026-10-23T08:14:40.000Z",
"updated_at": "2026-10-23T08:15:02.000Z",
"visitor": { "id": "vis_...", "name": "Lan", "...": "..." }
}
]
}

channel is one of web, zalo, zalo_personal, messenger, instagram, tiktok, tiktokshop, whatsapp, telegram, x, line or email. The same fields are used everywhere a conversation appears, including webhooks.

caution

Boolean query parameters are read as "present means true". Send unanswered=true to filter, and leave the parameter out otherwise; unanswered=false does not turn the filter off.

2. Read the timeline​

GET /chat/conversations/{id}/messages returns messages and events in seq order. Each item has kind: "message" or kind: "event".

Query parameterMeaning
after_seqReturn items after this seq (oldest first). Without it you get the latest page
limit1–200, default 100
include_privatetrue to include private notes. Leave it out to skip them
curl "https://api.hatang.dev/chat/conversations/cnv_123/messages?after_seq=10" \
-H "Authorization: Bearer ht_123..."
One message item
{
"kind": "message",
"id": "msg_...",
"conversation_id": "cnv_123",
"seq": 11,
"sender_type": "visitor",
"sender_id": "vis_...",
"sender_name": "Lan",
"visibility": "public",
"content_type": "text",
"content": "Shop còn size M không ạ?",
"attachments": [],
"metadata": {},
"delivery_status": null,
"delivery_error": null,
"read_at": null,
"created_at": "2026-10-23T08:15:02.000Z"
}

sender_type is visitor, agent, bot or system. Attachments carry a signed url that works for 24 hours; read the message again for a fresh link.

3. Reply or add a note​

POST /chat/conversations/{id}/messages posts a message as your team. Ha Tang delivers it on the conversation's own channel: the widget, Zalo, Messenger, email and so on.

FieldTypeNotes
contentstringRequired, 1–10,000 characters
visibilitypublic | privateDefault public. private is an internal note the customer never sees
sender_namestringName shown to the customer, up to 100 characters. Default Support
attachmentsstring[]Up to 5 ids of files already uploaded to the conversation
metadataobjectA rich card (product, order, payment, quick_replies), up to 4 KB

Send an Idempotency-Key header to make retries safe. A second request with the same key in the same conversation returns the original message with 200 and "duplicate": true instead of posting twice.

curl -X POST "https://api.hatang.dev/chat/conversations/cnv_123/messages" \
-H "Authorization: Bearer ht_123..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-8812-shipped" \
-d '{"content": "Đơn #8812 đã giao cho đơn vị vận chuyển, dự kiến tới vào thứ Sáu.", "sender_name": "Shop Trà Xanh"}'
201 Created
{ "object": "chat_message", "id": "msg_...", "seq": 15, "conversation_id": "cnv_123", "duplicate": false }

What can go wrong:

  • 409 conversation_closed: the conversation is closed and takes no more messages.
  • 422 validation_error: empty content, too long, or metadata over 4 KB.
  • Delivery on social channels is asynchronous. The API accepts the message right away; if the channel refuses it later (for example a 24-hour window has passed on Messenger or WhatsApp), the message gets delivery_status: "failed" and a delivery_error. Read the timeline or watch the inbox to see it. The channel pages explain each platform's rules.

To post product cards, use POST /chat/conversations/{id}/products with product_ids (up to 10) and an optional intro.

4. Assign, tag, snooze and resolve​

PATCH /chat/conversations/{id} changes one or more of these fields:

FieldValues
statusopen, pending, resolved, closed
assignee_idAn employee's user id, or null to unassign
tagsThe full list of tags (replaces the current tags; up to 20, each up to 40 characters)
member_idsThe full list of employees following the conversation (up to 20)
flaghidden (snooze), spam, or null to clear
curl -X PATCH "https://api.hatang.dev/chat/conversations/cnv_123" \
-H "Authorization: Bearer ht_123..." \
-H "Content-Type: application/json" \
-d '{"status": "resolved", "tags": ["shipping"]}'

The response is the updated conversation. Each change is recorded in the timeline as an event, attributed to the API key.

5. Get notified with webhooks​

Create a webhook endpoint (dashboard Webhooks, or POST /webhooks) and subscribe to the Support events you need. Each delivery is { "type", "created_at", "data" }, signed like every Ha Tang webhook (Webhooks).

EventWhen
chat.conversation.createdA new conversation starts on any channel
chat.conversation.assignedThe assignee changes
chat.conversation.resolved, chat.conversation.closed, chat.conversation.reopenedStatus changes
chat.conversation.handoffThe AI assistant hands the conversation to a person
chat.conversation.taggedA tag is added (one event per new tag, with tag)
chat.message.createdA public message is posted, by the customer, an agent, the bot or the API
chat.visitor.identifiedA visitor is identified
chat.ticket.created, chat.ticket.updated, chat.ticket.resolved, chat.ticket.breachedTicket changes and SLA breaches
chat.rating.createdA customer rates a conversation
chat.order.created, chat.order.updated, chat.order.paidOrders
chat.flow.webhook, chat.campaign.leadA flow's webhook step runs; a campaign form collects a lead
chat.zns.sent, chat.zns.failedZalo ZNS results

chat.message.created carries the message plus a conversation object. Private notes do not fire it.

Avoid reply loops

chat.message.created also fires for your own replies. If a bot answers this event, act only when data.sender_type is visitor.

Profile links put your own URLs next to every conversation, filled for the person in it. PUT /chat/profile-links (scope chat:write) replaces the whole list; GET /chat/profile-links (chat:read) returns it.

curl -X PUT "https://api.hatang.dev/chat/profile-links" \
-H "Authorization: Bearer ht_123..." \
-H "Content-Type: application/json" \
-d '{"data": [{"label": "Admin", "url": "https://example.com/admin/users/{{user_id}}"}, {"label": "Orders", "url": "https://example.com/admin/orders?email={{email}}"}]}'

The response is { "object": "list", "data": [...] } with an id (pl_…) on each link; send the ids back to keep them. Add site_ids to show a link only in some sites' conversations.

VariableValue
{{user_id}}The verified user_id from a signed identify; otherwise the one the visitor claimed, and agents see the link marked unverified (Identity verification)
{{email}}, {{phone}}, {{name}}The customer's contact details
{{customer_id}}, {{conversation_id}}, {{site_id}}Ha Tang ids
{{field.<key>}}A customer field
{{data.<key>}}The visitor's custom data (always marked unverified)

Rules: https:// only, variables only after the host (never in the domain), up to 500 characters per URL and 10 links. Anything else is a 422 validation_error. Values are URL-encoded, and a link with a variable that has no value is not shown.

More endpoints​

The same API covers the rest of Support. All paths start with https://api.hatang.dev/chat:

AreaPaths
Sites/sites, /sites/{id}, /sites/{id}/identity-secret
Visitors and live board/visitors/{id}, /visitors/{id}/conversations, /live, /live/greet
Customers/customers, /customers/{id}, /customers/{id}/merge, /customers/{id}/conversations, /customers/export.csv, /customer-fields
Tickets and SLA/tickets, /tickets/stats, /tickets/{id}, /sla
Profile links/profile-links
Automation/flows, /workflows, /campaigns, /routing-rules, /canned-responses
AI knowledge/sites/{id}/kb/sources, /sites/{id}/kb/files, /sites/{id}/kb/search
Commerce/products, /orders, /orders/{id}/payment-link
Zalo ZNS/zns/templates, /zns, /zns/bulk
Reports/reports, /ratings

Every request and response is in the REST API reference. The Node.js SDK wraps most of them under hatang.chat.*; see Node.js SDK.

Errors and limits​

Errors use the shared envelope { "statusCode", "name", "message" }. Branch on name, not on message.

StatusnameCause
401missing_api_keyNo Authorization header
403invalid_api_keyUnknown or revoked key
401restricted_api_keyThe key lacks the scope for this endpoint
403product_disabledThe Support module is off
404not_foundThe id does not exist in this business
409conversation_closedPosting into a closed conversation
422validation_errorThe body or query failed validation
429rate_limit_exceededOver the rate limit

Support endpoints allow 50 requests per second per business. See Errors and Limits.

Next steps​

View this page as Markdown