Get started
Build a bot
A bot is an agent of type bot: routing gives it chats like any agent, it replies through the same agent API, and it hands a chat to a person by transferring it. You bring the brains; mlivechat brings the chats.
Create the bot and get a token
An admin opens Team › Chatbots › Add chatbot: name, picture, groups with a priority, chat limit, handover timeout. The token (mlcb_…) is shown once. New token issues another and revokes every earlier one at once (open sockets close with 4001). Turning the bot off refuses its token (403), closes its sockets (4003) and moves its chats to people.
Priority per group decides when the bot gets chats:
| Priority | Label in the app | Meaning |
|---|---|---|
first |
Bot first | every new chat goes to the bot while it is under its chat limit |
normal |
In turn | the bot takes turns with the agents |
last |
Fallback | only when no person in the group can take the chat |
Bots never take chats from the queue; a full bot simply stops getting new ones.
Authenticate
Every request carries the token:
curl -X POST -H "Authorization: Bearer $MLC_BOT_TOKEN" -H 'Content-Type: application/json' \
-d '{}' https://app.staging.mlive.chat/v1/configuration/action/get_agentEvery method is one POST to /v1/agent/action/<method> (chats, events, tags, transfers, customers, routing status) or /v1/configuration/action/<method> (agents, groups, tags, canned responses) with a JSON body. The bot sees only chats assigned to it; anything else is not_found. Admin methods answer authorization.
Tell routing you are ready (a new bot starts not accepting):
curl -X POST -H "Authorization: Bearer $MLC_BOT_TOKEN" -H 'Content-Type: application/json' \
-d '{"status":"accepting_chats"}' https://app.staging.mlive.chat/v1/agent/action/set_routing_statusHear about chats: WebSocket or webhooks
Pick one. Both carry the same payloads.
WebSocket (no public URL needed)
POST /v1/ws_ticketwith the token →{ticket, url}(single use, 30 s).- Open
wss://<host>/v1/ws?v=1&ticket=<ticket>(bots send noOrigin). - Send
{"request_id":"1","action":"login","payload":{}}first. - Send
{"action":"ping"}every 25 s; reconnect with a new ticket on close (backoff 0.5 s → 30 s).
Pushes a bot cares about:
| Push | When | What to do |
|---|---|---|
user_added_to_chat (agent_id = you) |
routing gave you a chat | greet, or answer the opening message (get_chat) |
incoming_event |
a message or system message in your chat | answer author_type: customer messages; see postback and system_message_type |
chat_transferred |
the chat left you (take over, timeout) or came to you | stop / start answering |
chat_deactivated |
the chat closed | forget it |
c, _, _ := websocket.Dial(ctx, "wss://app.mlive.chat/v1/ws?v=1&ticket="+ticket, nil)
c.Write(ctx, websocket.MessageText, []byte(`{"request_id":"1","action":"login","payload":{}}`))
for {
_, raw, err := c.Read(ctx)
if err != nil { return err } // 4001: token revoked; 4003: bot turned off
var f struct{ Action string; Payload json.RawMessage }
json.Unmarshal(raw, &f)
// switch f.Action { case "user_added_to_chat", "incoming_event": … }
}Webhooks
A bot's webhooks only carry events about its chats (except customer_visit_started, below):
| Action | Sent when | Payload |
|---|---|---|
incoming_chat |
routing gives the bot a chat | {chat_id, seq, thread, agent_id} |
incoming_event |
a visitor message or system message in a chat the bot has now (not agents' or its own messages) | {chat_id, seq, thread_id, event} |
chat_transferred |
a chat the bot has or had in this thread changes hands | {chat_id, seq, thread, transferred_to, queue} |
chat_deactivated |
such a chat closes | {chat_id, seq, thread, reason} |
customer_visit_started |
a visitor opens your site with the widget and has no open visit (once per visit: reloads, more tabs and new pages in the same visit send nothing; a visit ends 60 s after the last tab closes) | {visit_id, visitor} |
customer_visit_started is the one action not tied to the bot's chats: there is no chat yet, so every bot subscribed to it hears every new visit. Organization webhooks (Settings → Webhooks) can subscribe to it too.
Each POST has the envelope {webhook_id, secret_key, action, organization_id, payload} and an X-Mlc-Signature: t=<unix>,v1=<hex> header: HMAC-SHA256 with the webhook's secret (shown once at creation) over "<t>." + raw body. Answer 2xx within 10 s; failures are retried 3 times within about a minute. Deliveries are unordered; use seq.
Verify every request before trusting it, and reject a t older than 5 minutes:
func verify(secret string, r *http.Request, body []byte) bool {
var t int64
var sig string
for _, part := range strings.Split(r.Header.Get("X-Mlc-Signature"), ",") {
if v, ok := strings.CutPrefix(part, "t="); ok { t, _ = strconv.ParseInt(v, 10, 64) }
if v, ok := strings.CutPrefix(part, "v1="); ok { sig = v }
}
if d := time.Since(time.Unix(t, 0)); d > 5*time.Minute || d < -5*time.Minute { return false }
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", t)
mac.Write(body)
want, err := hex.DecodeString(sig)
return err == nil && hmac.Equal(mac.Sum(nil), want)
}# The same check in a shell, for debugging (body in body.json, header value in $SIG):
t=${SIG#t=}; t=${t%%,*}; v=${SIG#*v1=}
printf '%s.' "$t" | cat - body.json | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | grep -q "$v" && echo okReply
curl -X POST -H "Authorization: Bearer $MLC_BOT_TOKEN" -H 'Content-Type: application/json' \
https://app.staging.mlive.chat/v1/agent/action/send_event -d '{
"chat_id": "'$CHAT'",
"event": { "type": "message", "text": "Orders ship within 1 business day.", "custom_id": "order-faq-1" }
}'custom_id is any string you choose; retrying with the same one never sends twice. Over the socket the same message goes in {"action":"send_event","payload":{"chat_id":…,"client_message_id":…,"event":{…}}}. Show you’re working on it with send_typing_indicator {chat_id, is_typing} (the same method on the socket). Message text is 1 to 4000 characters. A bot has the agent rate limit: 30 messages per 10 s.
Rich messages: quick replies
Add up to 10 buttons to a visible message:
{ "chat_id": "…", "event": { "type": "message", "text": "What can I help with?",
"quick_replies": [ { "title": "Shipping", "postback": "faq:shipping" },
{ "title": "Returns", "postback": "faq:returns" } ] } }title is 1 to 40 characters, postback 1 to 200. The widget shows the buttons under the newest message until the visitor writes; a tap sends title as the visitor's message with postback attached, which you get as event.postback. Agents see the buttons read-only in the agent app. Treat a postback as visitor input: it is not signed.
Hand over to a person
curl -X POST -H "Authorization: Bearer $MLC_BOT_TOKEN" -H 'Content-Type: application/json' \
https://app.staging.mlive.chat/v1/agent/action/transfer_chat \
-d '{"id":"'$CHAT'","target":{"type":"group","ids":["0"]}}'targetis a group (its accepting people, least loaded first; never another bot) or one agent ({"type":"agent","ids":["<agent id>"]}, who must be online and under their limit).- Nobody accepting in the group: the chat waits in that group's queue and the visitor sees their position.
- To leave the agent a note the visitor never sees, send it first with
send_eventand"visibility": "agents". - The thread is tagged
bot-transfer. Reports › Overview counts these as Bot handovers.
Two more ways a chat leaves the bot, both tagged bot-transfer:
- The visitor asks. While a bot has the chat the widget shows Talk to a human. The bot gets a system message with
system_message_type: "handover_requested"(socketincoming_event, and theincoming_eventwebhook). Transfer when you see it. If the bot hasn't within its handover timeout (default 20 s, set per bot), the server transfers to the chat's group itself. - A person takes over. Agents see a Take over button on any chat a bot is handling. The bot gets
chat_transferred(socket and webhook) and can no longer write in the chat (404).
Checklist before going live
- The token lives in your secret store, not the code; rotate it from Team › Chatbots if it leaks.
- Webhook signatures are verified and old timestamps rejected.
- Reconnects use a new ticket each time and back off; 4001/4003 stop the bot.
client_message_idis reused on retry.- The bot hands over on
handover_requested, and when it isn't sure. - The chat limit fits what your service can answer at once (1 to 500, default 50).