For the developers connecting a company's own systems to its AI customer service agent. Everything here works on its own: you can use one part, or all of them.
Customers chat with the agent inside your own app. Pick one of two ways.
Open this address in a full-screen web view (WKWebView on iOS, WebView on Android). It uses your brand colour and agent name, supports photos and files, and shows your team's replies after a hand-over.
https://YOUR-SITE/chat?c=CLIENT_ID&name=Mona&phone=%2B201001234567&order=1042
name, phone and order are optional. With the order number the agent starts from the right order. These values come from the device, so the agent treats them as hints, not as proof of identity.
Your app keeps its own chat design. Your server (never the app itself, so the API key stays secret) sends each customer message to us and gets the agent's reply back in the same call.
POST https://YOUR-SITE/api/v1
Authorization: Bearer ak_xxxxxxxxxxxxxxxxxxxxxxxx
{"op":"chat.message",
"customer":{"id":"u_48213","name":"Mona Adel","phone":"+201001234567"},
"order":"1042",
"text":"My order is an hour late",
"messageId":"msg_9f3a"}
Reply, usually in 2 to 6 seconds:
{"conversation":"app3fa9c1e0b2d4_m1x2",
"replies":["Sorry about the wait, Mona. Let me check order 1042 for you."],
"status":"open",
"handedOver":false,
"reason":"Late order"}
| Field | Meaning |
|---|---|
customer.id | Required. Your own ID for the customer. We keep one conversation per customer and start a new one after 24 hours of silence, or 2 hours after a chat is solved or closed. |
customer.name, phone, email | Optional. Since your server sends them for a logged-in user, the agent treats them as verified and uses the phone to look up their orders. |
customer.segment | Optional. Your own customer segment, for example VIP, Gold or Business. When the chat is handed over, it goes to the queue for that segment (set up in Team and access › Queues and skills). On the website chat, add data-segment="VIP" to the chat code instead. |
order | Optional. The order the customer opened the chat from. |
text, file | The message. file is {"name","type","data"} with base64 data, up to 3 MB: photos, voice notes (transcribed) and PDFs. |
messageId | Optional but recommended. The same ID sent twice is processed once, so retries are safe. |
context | Optional. Anything else the agent should know, in plain words, for example "VIP customer, 3 orders this week". |
new | Optional. true starts a fresh conversation. |
When handedOver is true, show the replies and tell the customer a person is joining. The agent then stays quiet. Keep sending the customer's messages with chat.message (the response will have no replies) and deliver your team's answers in one of two ways:
message.agent. Each reply from a person arrives as {"event":"message.agent","data":{"conversation","customerId","text","agent","file"}}, signed like every other event. Push it to the customer's phone.{"op":"chat.history","customer":{"id":"u_48213"},"since":1734000000000} every few seconds while the chat is handed over. Messages have from: customer, ai or agent.When the person closes the chat, the customer gets a short rating question in the same chat. You can also send a rating from your own screen: {"op":"chat.rate","conversation":"ID","rating":5}.
In-app chat allows up to 3,000 requests a minute per key and 30 messages a minute per customer.
The agent answers on WhatsApp, the website chat, Facebook Messenger and Instagram. There are three ways to connect it to your systems:
Your account manager sets these up in the console under the client's Channels tab. Send them the details below, or do it together on a call.
An action is one request to your API that the agent can make by itself. Each one has a name, a sentence describing when to use it, the inputs it needs, and the HTTP request to make. Ready templates exist for Shopify, WooCommerce, Zendesk, Freshdesk and the common order actions (status, cancel, refund request, change address).
| Field | Example |
|---|---|
| Method and address | GET https://api.yourshop.com/orders/{order_number} |
| Inputs | order_number (required). Inputs named in the address go in the path; others go in the query string or JSON body. |
| Authentication | Any headers (e.g. Authorization: Bearer …) or basic auth. Stored on our server only, never sent to the browser or the AI. |
| Body template (optional) | {"reason":"{reason}"}. Values are JSON-escaped for you. |
| Result path (optional) | data.order to use only part of your response. |
| Personal fields | name,phone,email,address. Hidden from the agent unless the customer's phone number matches the one on the record. |
| Ask first | Yes for anything that changes data. The agent must get a clear yes from the customer before the request is sent. |
| Input rules | Type (number, phone, email, date), minimum and maximum, allowed values and a format pattern. Checked on our server before anything reaches your system, so a refund above the limit or a malformed order number is never sent. |
| Who decides | The AI (after the customer's yes), or a person approves it first in the Agent Desk. |
| Proof of success | A field in your response that must have a given value, e.g. status = cancelled. Without it, the agent treats the change as not done. |
| Read back | A read action called right after the change, so the customer is told what your system now shows. |
Idempotency-Key header that stays the same for the same change in the same chat. Store it and ignore repeats, and a network retry can never refund twice. We also never send the same change twice in one chat, and cap each change action at a few per chat.https:// addresses on the public internet are called.Any JSON. Plain field names help the agent read it: status, eta, items, tracking_url. Keep responses small; we pass the first 6,000 characters to the agent. Return a 4xx with {"error":"Order not found"} when something doesn't exist.
We POST JSON to your address for the events you choose. Every request has these headers:
Content-Type: application/json
X-Event: conversation.escalated
X-Signature-256: sha256=<HMAC-SHA256 of the raw body, using your signing secret>
Reply with any 2xx within 5 seconds. If your system is down, we keep the event and retry with growing gaps for up to 2 days. Each event has a unique id: ignore ids you've already processed.
{
"event": "conversation.escalated",
"client": "acme",
"at": "2026-10-16T09:12:44.120Z",
"id": "ev_lx9k2a1b3c",
"data": { ... }
}
| Event | When | data |
|---|---|---|
conversation.escalated | The AI hands a chat over to a person | conversation, channel, reason, customer, phone, contactReason, messages (last 30: from, text, at) |
message.received | The customer writes again in a handed-over chat | conversation, channel, text, phone (WhatsApp) |
conversation.resolved | The AI resolves a chat by itself | conversation, channel, contactReason |
conversation.closed | A person closes a chat (Agent Desk or API) | conversation, by, outcome |
order.lookup | The agent looks up an order | conversation, order, found |
action.done | The agent completed a change in your system | conversation, action, input, result |
test.ping | Someone presses "Send a test event" | message |
import crypto from "node:crypto";
function valid(rawBody, header, secret) {
const want = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return header.length === want.length && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(want));
}
One endpoint. Every request is a POST with a JSON body and your API key:
POST https://YOUR-SITE/api/v1
Authorization: Bearer ak_xxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
The key is created in the console (Channels → Webhook and API) and shown once. Creating a new one stops the old one immediately.
| op | Body | Does |
|---|---|---|
conversation.get | {"conversation":"ID"} | Returns the conversation with all messages. |
conversation.reply | {"conversation":"ID","text":"…","agent":"Sara"} | Sends a reply to the customer on their channel, from a person in your help desk. |
conversation.close | {"conversation":"ID","outcome":"solved"} | Marks the chat handled. The next customer message goes back to the AI. |
orders.upsert | {"key":"order_number","rows":[{…}]} | Adds or updates up to 2,000 orders per call for the agent's order lookups. Any columns. |
message.send | {"to":"2010…","template":"order_delayed","language":"ar","variables":["1001","20 minutes"]} | Sends an approved WhatsApp template, for proactive updates. If the customer replies, the agent knows what was sent. |
curl -X POST https://YOUR-SITE/api/v1 \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"op":"conversation.reply","conversation":"wa201001234567_lx2k","text":"Your refund was approved today.","agent":"Sara"}'
Up to 120 requests a minute per key; above that you get 429 with Retry-After. Errors return a 4xx or 5xx with {"error":"…"}. WhatsApp only allows free-text replies within 24 hours of the customer's last message; after that, use message.send with a template.
https://YOUR-SITE/desk with their own email. They see handed-over chats from every channel in one queue, take one, read the AI's summary, use suggested and saved replies, look up orders, add notes and close with an outcome. Each agent is a paid seat.conversation.escalated and message.received, create a ticket from the transcript, and send replies back with conversation.reply. Close with conversation.close.A complete Node.js 18+ server with no dependencies. It receives our webhooks, checks the signature, and has the two endpoints most actions need. Replace the in-memory orders with your database.
// server.mjs: run with WEBHOOK_SECRET=... API_KEY=... ACTION_KEY=... node server.mjs
import http from "node:http";
import crypto from "node:crypto";
const { WEBHOOK_SECRET = "", API_KEY = "", ACTION_KEY = "", PLATFORM = "https://YOUR-SITE" } = process.env;
const orders = { "1001": { status: "Out for delivery", eta: "Today 6–8 pm", phone: "201001234567", name: "Mona" } };
const json = (res, code, obj) => { res.writeHead(code, { "Content-Type": "application/json" }); res.end(JSON.stringify(obj)); };
const body = req => new Promise(ok => { const c = []; req.on("data", d => c.push(d)); req.on("end", () => ok(Buffer.concat(c))); });
const platform = (op, data) => fetch(PLATFORM + "/api/v1", { method: "POST",
headers: { Authorization: "Bearer " + API_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ op, ...data }) }).then(r => r.json());
http.createServer(async (req, res) => {
const url = new URL(req.url, "http://x");
// Events from the AI agent
if (req.method === "POST" && url.pathname === "/hooks/support") {
const raw = await body(req), sig = String(req.headers["x-signature-256"] || "");
const want = "sha256=" + crypto.createHmac("sha256", WEBHOOK_SECRET).update(raw).digest("hex");
if (sig.length !== want.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want))) return json(res, 401, { error: "bad signature" });
const ev = JSON.parse(raw);
json(res, 200, { ok: true }); // answer fast, work after
if (ev.event === "conversation.escalated") {
console.log("New hand-over", ev.data.conversation, ev.data.reason);
// createTicket(ev.data) ... later: await platform("conversation.reply", { conversation: ev.data.conversation, text: "…", agent: "Sara" });
}
return;
}
// Actions the agent calls (set Authorization: Bearer ACTION_KEY in the console)
if (req.headers.authorization !== "Bearer " + ACTION_KEY) return json(res, 401, { error: "unauthorised" });
const m = url.pathname.match(/^\/orders\/([\w-]+)(\/cancel)?$/);
if (m) {
const o = orders[m[1]];
if (!o) return json(res, 404, { error: "Order not found" });
if (req.method === "GET") return json(res, 200, o);
if (req.method === "POST" && m[2]) {
if (o.status !== "Received") return json(res, 409, { error: "Already being prepared, can't cancel" });
o.status = "Cancelled"; return json(res, 200, { ok: true, status: o.status });
}
}
json(res, 404, { error: "not found" });
}).listen(8080, () => console.log("Listening on :8080"));
Send finished chats, tickets, emails or call recordings from any system. Each is scored against the client's approved scorecard and knowledge base; uncertain results go to the client's reviewers. Small sends are scored straight away.
POST https://YOUR-SITE/api/v1
Authorization: Bearer ak_xxxxxxxx
{"op":"qa.submit","interaction":{
"id":"TICKET-1042", // your ID: sending it again replaces the old copy
"channel":"chat", // chat, email, ticket or call
"queue":"Refunds", // optional: picks the scorecard for this queue
"agent":{"id":"sara@acme.com","name":"Sara"},
"csat":4, // optional, 1 to 5
"messages":[
{"from":"customer","text":"My order is late","at":"2026-10-16T09:00:00Z"},
{"from":"agent","text":"Sorry about that, let me check","at":"2026-10-16T09:01:00Z"}]}}
{"op":"qa.submit","items":[…]}. Instead of messages you can send "transcript":"…", or for calls "audioUrl":"https://…" (transcribed first; speakers aren't separated).{"op":"qa.get","externalId":"TICKET-1042"} returns the score, each attribute's result, reason, quoted evidence and confidence, the summary and coaching. Or subscribe to the qa.completed webhook event.{"op":"qa.process"} scores anything still queued; {"op":"qa.report","from":"2026-10-01","to":"2026-11-01"} returns results by attribute and agent.Questions: sales@conversley.com