Voipai Interactions API
Push customer interactions into Voipai and read the AI intelligence back — analyses, recurring issues, and the recommendations they drive. Each workspace runs on its own server; point this page at yours below, then authenticate with your API key.
Configuration
Enter the base URL of your Voipai server (given to you at onboarding — e.g. https://s24.gooyatel.com/api/v1). Everything on this page — samples and live calls — targets it. Stored only in your browser.
Authentication
Every request carries a tenant API key as a bearer token. Issue and revoke keys in your panel under Settings → Integrations. Keep the key server-side; it is shown only once. This is a secret key (vpk_…) for server-to-server calls — never put it in a browser. For the in-browser website chatbot use the separate public widget key instead.
Authorization: Bearer <your-api-key>/pingscope: readVerify a key
A cheap health/auth check: confirms your key and base URL and echoes back the workspace it is bound to and the scopes it carries. Start here when wiring an integration.
curl -X GET "https://YOUR-SERVER/api/v1/ping" \
-H "Authorization: Bearer $VOIPAI_KEY"Set your server base URL and API key at the top to enable live calls.
Example response
{
"ok": true,
"workspace": "Acme Support",
"workspace_id": 42,
"scopes": [
"read",
"write"
],
"key_prefix": "vpk_abcd",
"server_time": "2026-07-27T12:00:00+00:00"
}/interactionsscope: writeIngest an interaction
Push a ticket or chat into Voipai. It is queued for AI analysis, embedding, case-linking and clustering — the same pipeline calls flow through.
curl -X POST "https://YOUR-SERVER/api/v1/interactions" \
-H "Authorization: Bearer $VOIPAI_KEY" \
-H "Content-Type: application/json" \
-d '{"channel":"ticket","customer":"[email protected]","kind":"email","started_at":"2026-07-20T10:00:00","subject":"Internet down","body":"My internet has been down since last night.","agent_reply":"We opened a ticket and are checking the line."}'Set your server base URL and API key at the top to enable live calls.
Example response
{
"id": 4821,
"status": "accepted",
"channel": "ticket"
}/interactionsscope: readList recent interactions
A page of interactions, newest first. Keyset pagination: pass `before` (an id) and read `next_before` from the response to walk back through history.
Parameters
| limit | query | optional | Max results (default 50, max 200) |
| before | query | optional | Return interactions with id < this |
| channel | query | optional | Filter by channel (e.g. ticket) |
curl -X GET "https://YOUR-SERVER/api/v1/interactions" \
-H "Authorization: Bearer $VOIPAI_KEY"Set your server base URL and API key at the top to enable live calls.
Example response
{
"interactions": [
{
"id": 4821,
"channel": "ticket",
"direction": "inbound",
"started_at": "2026-07-20T10:00:00+00:00",
"pipeline_status": "analyzed",
"customer_id": 317,
"external_ref": "ZD-9912"
}
],
"next_before": 4820
}/interactions/{id}scope: readGet an interaction & its analysis
Read one interaction with the AI’s structured read: intent, sentiment, QA, churn/escalation signals, resolution, topics and a summary.
Parameters
| id | path | required | Interaction id |
curl -X GET "https://YOUR-SERVER/api/v1/interactions/1" \
-H "Authorization: Bearer $VOIPAI_KEY"Set your server base URL and API key at the top to enable live calls.
Example response
{
"id": 4821,
"channel": "ticket",
"direction": "inbound",
"started_at": "2026-07-20T10:00:00+00:00",
"pipeline_status": "analyzed",
"customer_id": 317,
"analysis": {
"status": "ready",
"intent": "internet outage",
"sentiment_customer": -1,
"qa_score": 6,
"churn_signal": false,
"escalation_signal": false,
"resolution": "follow_up_promised",
"topics": [
"internet",
"outage"
],
"summary": "Customer reported an outage since last night; a line check was promised."
}
}/issuesscope: readList active issues
The recurring themes behind your interactions, ranked by impact — the systemic problems worth fixing once.
Parameters
| limit | query | optional | Max results (default 50) |
curl -X GET "https://YOUR-SERVER/api/v1/issues" \
-H "Authorization: Bearer $VOIPAI_KEY"Set your server base URL and API key at the top to enable live calls.
Example response
{
"issues": [
{
"id": 12,
"label": "internet slowness",
"size": 22,
"impact": 43,
"status": "active",
"last_seen": "2026-07-27T14:06:20+00:00"
}
]
}/recommendationsscope: readList recommendations
Pending, actionable recommendations generated from risk signals and issues — the decision queue, over the wire.
Parameters
| limit | query | optional | Max results (default 50) |
curl -X GET "https://YOUR-SERVER/api/v1/recommendations" \
-H "Authorization: Bearer $VOIPAI_KEY"Set your server base URL and API key at the top to enable live calls.
Example response
{
"recommendations": [
{
"id": 88,
"kind": "risk_intervention",
"priority": 2,
"title": "Reach out to an at-risk customer",
"body": "A recent interaction signalled churn. Follow up before the customer leaves.",
"customer_id": 317,
"created_at": "2026-07-27T12:00:00+00:00"
}
]
}Website Chatbot Widget
public keyDrop a chat widget on your own website that answers from your Voipai knowledge base — and, when you connect your site, from its live products and pages too. Unlike the Interactions API above, this uses a public key (wgk_…) that ships in your page’s JavaScript. It grants no access to your data — every call is gated server-side by the widget’s origin allowlist and rate limits. Calls run from the browser under CORS (no bearer token, no cookies), and answers can be streamed. Create a widget and set its allowed domains in your panel under چتبات.
On WordPress? Install Gooyatel WP Client instead of writing any of this. It renders the widget on every page, and gives Gooyatel a signed, read-only endpoint so answers can quote your real WooCommerce prices and stock. Nothing below is needed — the plugin does it.
Embed on your site
A minimal, dependency-free streaming example. Paste the key from your panel; the visitor’s messages stream a grounded answer, and the returned session_id keeps context across turns. Render links as clickable elements rather than pulling URLs out of the answer text — the answer is model output, the links are not.
<!-- گویاتل chat widget — minimal streaming example (drop into your page) -->
<div id="gt-chat-log" dir="rtl"></div>
<form id="gt-chat-form"><input id="gt-chat-input" autocomplete="off" /></form>
<script>
const WIDGET_KEY = "wgk_your_public_key"; // PUBLIC — safe to ship in the page
const BASE = "https://YOUR-SERVER/api/widget";
let sessionId = null;
async function ask(message) {
const res = await fetch(BASE + "/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: WIDGET_KEY, message, session_id: sessionId }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", bubble = null;
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split("\n\n"); buf = frames.pop();
for (const frame of frames) {
const ev = JSON.parse(frame.replace(/^data: /, ""));
if (ev.session_id) sessionId = ev.session_id; // persist context
if (ev.delta) { (bubble ??= addBubble("bot")).textContent += ev.delta; }
if (ev.done) renderLinks(ev.links); // products / pages to click
if (ev.error) addBubble("bot").textContent = ev.error;
}
}
}
function addBubble(who) {
const el = document.createElement("div");
el.className = "gt-" + who;
document.getElementById("gt-chat-log").appendChild(el);
return el;
}
// Links arrive as data, never inside the answer text — build them as elements
// so a reply can never inject markup, and so the URL is always exactly right.
function renderLinks(links) {
for (const link of links || []) {
const a = document.createElement("a");
a.href = link.url;
a.target = "_blank";
a.rel = "noopener noreferrer";
// link.was is the pre-sale price — strike it, never print it beside the
// new one as a second number.
a.textContent = link.was
? link.title + " — " + link.meta + " (was " + link.was + ")"
: link.meta ? link.title + " — " + link.meta : link.title;
document.getElementById("gt-chat-log").appendChild(a);
}
}
document.getElementById("gt-chat-form").addEventListener("submit", (e) => {
e.preventDefault();
const input = document.getElementById("gt-chat-input");
if (!input.value.trim()) return;
addBubble("user").textContent = input.value;
ask(input.value);
input.value = "";
});
</script>/api/widget/chatpublic · CORSGrounded answer (JSON)
One grounded answer over your knowledge base and, if a site connector is configured, your live site. Returns the answer text, any products or pages to link to, and a session id — send that id back on the next message to keep conversational context.
Request body
{
"key": "wgk_your_public_key",
"message": "ساعت کاری شما چیست؟",
"session_id": null,
"contact": "[email protected]"
}Example (JavaScript)
const res = await fetch("https://YOUR-SERVER/api/widget/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
key: "wgk_your_public_key",
message: "ساعت کاری شما؟",
session_id: null, // returned in the reply — send it back to keep context
contact: "[email protected]" // optional: links the chat to a customer profile
}),
});
const data = await res.json();
// { answer, links: [{ type, title, url, meta, was, image }], session_id }Example response
{
"answer": "ساعت کاری ما ۹ تا ۱۷ است.",
"links": [
{
"type": "product",
"title": "میز اداری",
"url": "https://example.com/product/desk/",
"meta": "۲٬۵۰۰٬۰۰۰ تومان",
"was": "۲٬۹۰۰٬۰۰۰ تومان",
"image": "https://example.com/wp-content/uploads/desk.jpg"
}
],
"session_id": "a1b2c3d4"
}/api/widget/chat/streampublic · CORSGrounded answer (streaming · SSE)
The same answer, streamed token-by-token as Server-Sent Events for a live typing effect. The first event carries the session id, then deltas, then a final done event with any links.
Request body
{
"key": "wgk_your_public_key",
"message": "ساعت کاری شما چیست؟",
"session_id": "a1b2c3d4"
}Consume the stream (JavaScript)
const res = await fetch("https://YOUR-SERVER/api/widget/chat/stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "wgk_your_public_key", message: "ساعت کاری شما؟", session_id: null }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split("\n\n"); buf = frames.pop();
for (const frame of frames) {
const ev = JSON.parse(frame.replace(/^data: /, ""));
if (ev.session_id) console.log("session", ev.session_id);
if (ev.delta) process.stdout.write(ev.delta);
if (ev.done) console.log("\nlinks", ev.links);
}
}Event stream
data: {"session_id": "a1b2c3d4"}
data: {"delta": "ساعت کاری "}
data: {"delta": "ما ۹ تا ۱۷ است."}
data: {"done": true, "links": [{"type": "product", "title": "میز اداری", "url": "https://example.com/product/desk/", "meta": "۲٬۵۰۰٬۰۰۰ تومان", "was": "۲٬۹۰۰٬۰۰۰ تومان", "image": "https://example.com/…/desk.jpg"}]}/api/widget/configpublic · CORSWidget bootstrap
The widget’s public name and greeting — enough to render the launcher and opening bubble before the first message is sent.
Parameters
| key | query | required | The widget public key (wgk_…) |
Example (JavaScript)
const res = await fetch("https://YOUR-SERVER/api/widget/config?key=wgk_your_public_key");
const { name, greeting } = await res.json();Example response
{
"name": "پشتیبانی اکمی",
"greeting": "سلام! چطور میتونم کمکتون کنم؟"
}