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

Don't want to build UI? Drop the one-line embeddable widget on your site instead (Dashboard → Channels → Website widget). This guide is for teams who want their own interface.

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).

Rotate a key today by deleting & recreating it in Channels. One-click rotation is on the roadmap.

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

eventdatado
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. statebot | 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.
This is a POST that returns SSE, so the browser 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": "Չափից շատ հաղորդագրություն. փորձեք մեկ րոպեից" } }
HTTPcodemeaning
400invalid_inputempty message, or > 4000 chars
401unauthenticatedmissing/invalid key or slug, or hosted page disabled
404not_foundunknown conversation
429rate_limitedover 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

EndpointAuthPurpose
POST /v1/chatkey 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}/ratingkey or ?slug=submit 👍/👎 CSAT