# Voipai Interactions API

> Push customer interactions (tickets, chats) into Voipai and read the AI
> intelligence back: structured analyses, recurring issues, and the
> recommendations they drive. Each workspace runs on its own server.

## Base URL

Every workspace has its own Voipai server. Replace `<YOUR-SERVER>` with the host
you were given at onboarding (for example `s24.gooyatel.com`):

```
https://<YOUR-SERVER>/api/v1
```

Each server also serves this contract at:
- `https://<YOUR-SERVER>/api/v1/openapi.json` — OpenAPI 3.1 spec
- `https://<YOUR-SERVER>/api/v1/docs.md` — this document, scoped to that server

## Authentication

Every request carries a tenant API key as a bearer token:

```
Authorization: Bearer <your-api-key>
```

Issue and revoke keys in the panel under **Settings → Integrations**. The secret
is shown once at creation. Scopes gate access: `read` for the GET endpoints,
`write` for posting interactions.

---

## `GET /ping`

A cheap health/auth check. Confirms the key and base URL and echoes the
workspace it is bound to and the scopes it carries. Scope: `read`.

**Response** `200`

```json
{
  "ok": true,
  "workspace": "Acme Support",
  "workspace_id": 42,
  "scopes": ["read", "write"],
  "key_prefix": "vpk_abcd",
  "server_time": "2026-07-27T12:00:00+00:00"
}
```

---

## `POST /interactions`

Ingest a ticket or chat. It is queued for AI analysis, embedding, case-linking
and clustering — the same pipeline calls flow through. Scope: `write`.

**Request body**

```json
{
  "channel": "ticket",
  "customer": "user@example.com",
  "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."
}
```

Only `body` (or `agent_reply`) is required. `channel` defaults to `ticket`;
`kind` (`email`/`phone`) is inferred from `customer` when omitted; `started_at`
defaults to now.

**Response** `201`

```json
{ "id": 4821, "status": "accepted", "channel": "ticket" }
```

**Errors:** `400` empty/invalid body · `401` missing/invalid key · `429` rate limited.

---

## `GET /interactions`

A page of interactions, newest first. Scope: `read`.

**Parameters:** `limit` (query, default 50, max 200) · `before` (query — return
interactions with `id` < this) · `channel` (query — filter by channel).

Keyset pagination: read `next_before` from the response and pass it as `before`
to fetch the next page; it is `null` at the end.

**Response** `200`

```json
{
  "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
}
```

---

## `GET /interactions/{id}`

Read one interaction with the AI's structured analysis. Scope: `read`.

**Parameters:** `id` (path, required) — interaction id.

**Response** `200`

```json
{
  "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."
  }
}
```

---

## `GET /issues`

Active recurring issues, ranked by impact — the systemic problems worth fixing
once. Scope: `read`.

**Parameters:** `limit` (query) — max results, default 50.

**Response** `200`

```json
{
  "issues": [
    {
      "id": 12,
      "label": "internet slowness",
      "size": 22,
      "impact": 43.0,
      "status": "active",
      "last_seen": "2026-07-27T14:06:20+00:00"
    }
  ]
}
```

---

## `GET /recommendations`

Pending, actionable recommendations generated from risk signals and issues — the
decision queue, over the wire. Scope: `read`.

**Parameters:** `limit` (query) — max results, default 50.

**Response** `200`

```json
{
  "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"
    }
  ]
}
```

---

## Example (cURL)

```bash
curl -X POST "https://<YOUR-SERVER>/api/v1/interactions" \
  -H "Authorization: Bearer $VOIPAI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel":"ticket","customer":"user@example.com","body":"Internet down"}'
```

Interactive documentation with a live "Try it": https://gooyatel.com/docs

---

# Website Chatbot Widget

A separate, **embeddable** assistant you drop onto your own website so your
visitors can self-serve answers from your knowledge base — grounded, text-only,
streaming-capable. It uses a **public widget key** (`wgk_…`), not the Bearer API
key: the widget key ships in your page's JavaScript, so it is safe to expose but
is locked to the **allowed origins** you configure and is rate-limited per IP and
per key. Create a widget and its key in the panel under **Settings → Integrations
→ Website chatbot**.

Base URL (per workspace): `https://<YOUR-SERVER>/api/widget`

## `POST /api/widget/chat`

One grounded answer.

**Request**
```json
{ "key": "wgk_...", "message": "ساعت کاری شما چند است؟", "session_id": "optional-thread-id", "contact": "user@example.com" }
```
The key may instead be sent as the `X-Widget-Key` header. `session_id` threads a
conversation; omit it on the first call and reuse the `session_id` returned.
`contact` is optional (an email or phone the visitor gives) — when present, the
finalized conversation links to that customer's profile in your analytics.

**Response** `200`
```json
{
  "answer": "ساعت کاری ما ۹ تا ۱۷ است.",
  "citations": [{ "document": "FAQ", "chunk_id": 3, "score": 0.81 }],
  "session_id": "K4nR..."
}
```
**Errors:** `401` invalid key · `403` origin not allowed · `429` rate limited.

## `POST /api/widget/chat/stream`

The same, streamed as **Server-Sent Events** (`text/event-stream`). Each event is
`data: {json}`: `{"session_id": "..."}` first, then `{"delta": "…"}` tokens, then
`{"done": true, "citations": [...]}`.

```js
const res = await fetch("https://<YOUR-SERVER>/api/widget/chat/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ key: "wgk_...", message: "سلام" }),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  for (const line of dec.decode(value).split("\n")) {
    if (line.startsWith("data:")) {
      const ev = JSON.parse(line.slice(5).trim());
      if (ev.delta) process.stdout.write(ev.delta);
    }
  }
}
```

## `GET /api/widget/config?key=wgk_...`

Bootstrap the widget UI: returns `{ "name": "...", "greeting": "..." }`.

> Requests must be sent from one of the widget's **allowed origins** (the browser
> sends the `Origin` header automatically). Drop-in React and WordPress plugins
> are coming; today you embed with the fetch calls above.
