Build on HighChat
Put your own chat interface — web, iOS, Android, or server-side — on top of a HighChat bot using the public /v1 API. Streaming replies, conversation memory, live-agent handoff, and CSAT.
Base URL https://app.highchat.am
Quickstart
Create a key in Dashboard → Channels → REST API, then stream a reply:
curl -N https://app.highchat.am/v1/chat \
-H "Authorization: Bearer hc_xxx" \
-H "Content-Type: application/json" \
-d '{"message":"Բարև, ի՞նչ ժամերի եք աշխատում"}'
Authentication
Identify the bot in one of two ways.
A · Client API key recommended
Enable REST API (or Website widget) in Channels; the key shows once. Send it on every call:
Authorization: Bearer hc_xxxxxxxxxxxxxxxxxxxx
A key is a publishable client credential — scoped to one bot, rate-limited, safe to ship in a browser bundle or mobile app (like the widget's key). It is not a server secret: it can't read your dashboard, knowledge, or other bots. Want it off the client entirely? Proxy through your backend (see examples).
B · Public slug (no key)
If you enabled the bot's Hosted chat page (Channels → Public chat page), call the endpoints with ?slug=your-slug and no header — the same surface that powers highchat.am/c/your-slug.
Send a message — POST /v1/chat
Streams the assistant reply token-by-token over Server-Sent Events.
POST https://app.highchat.am/v1/chat
Authorization: Bearer hc_xxx # or POST /v1/chat?slug=your-slug (no header)
Content-Type: application/json
{
"message": "Բարև, ի՞նչ ժամերի եք աշխատում", // required, 1–4000 chars
"conversation_id": "…", // optional — continue a thread
"visitor_ref": "user-42" // optional — your end-user id, ≤200 chars
}
Event stream
Response is text/event-stream; each event is event: <name> + data: <json>:
| event | data | do |
|---|---|---|
meta | { conversation_id } | Emitted first. Store conversation_id and send it back next time to keep the thread. |
delta | { text } | A chunk of the reply — append to the current bubble. |
done | { message_id, state, ts } | Reply finished. state ∈ bot | pending_human | closed. |
human | { message, ts } | A human agent took over; the bot won't answer. Show message, then poll. |
error | { message } | Show it — a user-safe Armenian string. |
ping | {} | Keepalive. Ignore. |
EventSource API (GET-only) won't work — read the response body as a stream, as in the examples. Omit conversation_id to start a new conversation; reuse the one from meta to continue.Live-agent handoff
When the bot escalates (a human event, or done with state: pending_human), a human replies from the dashboard Inbox. Poll for those replies:
GET https://app.highchat.am/v1/conversations/{id}/messages?after={ISO-ts}
Authorization: Bearer hc_xxx # or ?slug=your-slug&after=…
{
"state": "human",
"messages": [ { "id":"…", "role":"assistant", "content":"…", "created_at":"2026-06-14T10:00:00.000Z" } ],
"ts": "2026-06-14T10:00:05.000Z"
}
Pass the newest created_at as after to get only new messages. Poll every ~4s while state is human/pending_human; stop when it returns to bot or closed.
CSAT rating — POST /v1/conversations/{id}/rating
POST https://app.highchat.am/v1/conversations/{id}/rating
Authorization: Bearer hc_xxx
Content-Type: application/json
{ "rating": 1, "comment": "շատ օգտակար էր" } // 1 = 👍, 0 = 👎 ; comment optional
Ratings show up in dashboard Analytics and as a filter in Conversations.
Errors & limits
One error shape; message is a user-presentable Armenian string:
{ "error": { "code": "rate_limited", "message": "Չափից շատ հաղորդագրություն. փորձեք մեկ րոպեից" } }
| HTTP | code | meaning |
|---|---|---|
| 400 | invalid_input | empty message, or > 4000 chars |
| 401 | unauthenticated | missing/invalid key or slug, or hosted page disabled |
| 404 | not_found | unknown conversation |
| 429 | rate_limited | over 30 messages/min for this key/bot — back off & retry |
CORS: /v1 allows any origin — call it directly from a browser app. No cookies are used.
Examples
const API = "https://app.highchat.am";
const KEY = "hc_xxx"; // or use ?slug=… and drop the header
let conversationId = null;
async function send(message, onDelta) {
const res = await fetch(`${API}/v1/chat`, {
method: "POST",
headers: { "content-type": "application/json", authorization: "Bearer " + KEY },
body: JSON.stringify({ conversation_id: conversationId, message }),
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", event = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n"); buf = lines.pop();
for (const line of lines) {
if (line.startsWith("event:")) event = line.slice(6).trim();
else if (line.startsWith("data:")) {
const data = JSON.parse(line.slice(5).trim());
if (event === "meta") conversationId = data.conversation_id;
else if (event === "delta") onDelta(data.text);
else if (event === "error") console.error(data.message);
}
}
}
}
function useHighChat(apiKey) {
const convId = useRef(null);
const [reply, setReply] = useState("");
async function send(message) {
setReply("");
const res = await fetch("https://app.highchat.am/v1/chat", {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
body: JSON.stringify({ conversation_id: convId.current, message }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", ev = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const lines = buf.split("\n"); buf = lines.pop();
for (const l of lines) {
if (l.startsWith("event:")) ev = l.slice(6).trim();
else if (l.startsWith("data:")) {
const d = JSON.parse(l.slice(5).trim());
if (ev === "meta") convId.current = d.conversation_id;
else if (ev === "delta") setReply((r) => r + d.text);
}
}
}
}
return { reply, send };
}
func send(_ message: String) async throws {
var req = URLRequest(url: URL(string: "https://app.highchat.am/v1/chat")!)
req.httpMethod = "POST"
req.setValue("Bearer hc_xxx", forHTTPHeaderField: "Authorization")
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpBody = try JSONSerialization.data(withJSONObject: [
"message": message, "conversation_id": conversationId as Any
])
let (bytes, _) = try await URLSession.shared.bytes(for: req)
var event = ""
for try await line in bytes.lines {
if line.hasPrefix("event:") { event = String(line.dropFirst(6)).trimmingCharacters(in: .whitespaces) }
else if line.hasPrefix("data:") {
let json = String(line.dropFirst(5)).trimmingCharacters(in: .whitespaces)
let d = (try? JSONSerialization.jsonObject(with: Data(json.utf8))) as? [String: Any] ?? [:]
if event == "meta" { conversationId = d["conversation_id"] as? String }
else if event == "delta" { appendToBubble(d["text"] as? String ?? "") }
}
}
}
val body = """{"message":${JSONObject.quote(message)},"conversation_id":${
conversationId?.let { JSONObject.quote(it) } ?: "null"}}"""
val req = Request.Builder()
.url("https://app.highchat.am/v1/chat")
.addHeader("Authorization", "Bearer hc_xxx")
.post(body.toRequestBody("application/json".toMediaType()))
.build()
client.newCall(req).execute().use { res ->
val src = res.body!!.source()
var event = ""
while (!src.exhausted()) {
val line = src.readUtf8Line() ?: break
when {
line.startsWith("event:") -> event = line.substring(6).trim()
line.startsWith("data:") -> {
val d = JSONObject(line.substring(5).trim())
when (event) {
"meta" -> conversationId = d.optString("conversation_id")
"delta" -> appendToBubble(d.optString("text"))
}
}
}
}
}
// Keep the key on YOUR server; your app talks to your backend.
app.post("/chat", async (req, res) => {
const upstream = await fetch("https://app.highchat.am/v1/chat", {
method: "POST",
headers: { "content-type": "application/json",
authorization: `Bearer ${process.env.HIGHCHAT_KEY}` },
body: JSON.stringify(req.body),
});
res.setHeader("content-type", "text/event-stream");
upstream.body.pipe(res); // stream SSE straight through
});
Quick reference
| Endpoint | Auth | Purpose |
|---|---|---|
POST /v1/chat | key or ?slug= | send a message, stream the reply (SSE) |
GET /v1/conversations/{id}/messages?after= | key or ?slug= | poll for live-agent replies |
POST /v1/conversations/{id}/rating | key or ?slug= | submit 👍/👎 CSAT |