Events

What your server receives, and how to check it came from HAI.

HAI posts each event to your events address as JSON, with the headers HAI-Event-Id and HAI-Signature. Answer any 2xx within 10 seconds. Anything else is tried again for up to 24 hours; a 4xx other than 401, 403, 408 and 429 ends it at once.

Check the signature

HAI-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>. Refuse anything older than five minutes and anything that does not match; answer 401. The portal's signature check tests that your server does.

# Python
import hmac, hashlib, time
def valid(secret: str, header: str, body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:
        return False
    mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, v1)
// Node.js
const crypto = require("crypto");
function valid(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const mac = crypto.createHmac("sha256", secret).update(parts.t + ".").update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(parts.v1 || ""));
}
// Go
func valid(secret, header string, body []byte) bool {
	var t, v1 string
	for _, p := range strings.Split(header, ",") {
		k, v, _ := strings.Cut(p, "=")
		if k == "t" { t = v } else if k == "v1" { v1 = v }
	}
	sec, err := strconv.ParseInt(t, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-sec)) > 300 { return false }
	m := hmac.New(sha256.New, []byte(secret))
	m.Write([]byte(t + "."))
	m.Write(body)
	return hmac.Equal([]byte(hex.EncodeToString(m.Sum(nil))), []byte(v1))
}

Check the raw body, before any JSON parsing. To drop an event you already handled, use the id inside the signed body (the same value as HAI-Event-Id): a header is not covered by the signature, so someone who recorded an event could resend it under a new header id within the five minutes.

Event types

TypeWhen
message.createdA message in one of its chats (message.history is true for one from before it joined).
message.unreadableA message that was not sealed for the agent, with a reason.
message.editedSomeone edited a message; the event carries the new message.
message.deletedSomeone deleted a message for everyone (message_id).
message.reactionA reaction set or removed (reaction.emoji empty).
sync.finishedA members sync ended (run).
member.left, member.removed_by_admin, member.joined_haiMembers it manages.
agent.removed, agent.not_admin, agent.admin_againA group admin changed the agent's place.
group.full, group.goneThe group reached 1,024 members, or was deleted.
testFrom the page's “Send a test event”.

A message

FieldMeaning
message.kindtext, image, video, gif, audio, file, sticker, location, contact, poll.
message.senderuid, name, and phone when allowed (see How HAI works).
message.text, caption, mentionsThe text; mention offsets in UTF-16 units.
message.addressed_to_agentThe message mentions the agent or replies to it.
message.reply_toThe quoted message's id and sender.
message.mediaurl and thumbnail_url to fetch with the agent's key, mime_type, size, width, height, duration, file_name.
message.location, contact, pollThe location, the contact card, the poll.
message.expires_atWhen the chat's timer ends the message.
message.historySent before the agent joined (only when a group admin shared the earlier messages).