# Wusper Chat API v1 (Bearer)

Base URL: `https://chat.wusper.com`

**Human docs (rendered):** [`/api-docs`](https://chat.wusper.com/api-docs)

## Auth

```http
Authorization: Bearer wc_YOUR_SECRET
Content-Type: application/json
```

- Keys are created in **設置 → API** (private / delegate / bot).
- Secrets are shown **once** on create / regenerate.
- Bot **share codes** (`wbot_…`) claim a bot as a friend in the UI — they are **not** Bearer tokens.
- Rate limit: **60 req/min** per key.

OpenAPI: [`/docs/openapi-v1.yaml`](./openapi-v1.yaml)  

**API package 下載（設置 → API 一鍵 zip）:** [`/api-packages/wusper-chat-api-packages.zip`](../api-packages/wusper-chat-api-packages.zip)

```bash
# JS / npm
npm install github:wusui1325-dotcom/chat-wusper#main:packages/wusper-chat-sdk
npm install https://chat.wusper.com/api-packages/wusper-chat-sdk.tgz

# Python
pip install https://chat.wusper.com/api-packages/wusper_chat-0.1.0-py3-none-any.whl
pip install "git+https://github.com/wusui1325-dotcom/chat-wusper.git#subdirectory=packages/wusper-chat-python"

# Linux .deb
curl -fsSL -O https://chat.wusper.com/api-packages/wusper-chat-sdk_0.1.0_all.deb
sudo apt install ./wusper-chat-sdk_0.1.0_all.deb
```

---


---

## Realtime inbound (WebSocket)

All three key types can open a persistent WebSocket to receive new messages immediately (no polling). Webhooks remain available; use either or both. **Deduped / idempotent sends are not re-broadcast.**

**URL:** `wss://chat.wusper.com/api/v1/ws`

**Auth (either):**

- Header: `Authorization: Bearer wc_YOUR_SECRET` (Node/`ws` clients)
- Query: `?access_token=wc_YOUR_SECRET` (browsers / simple clients)

**Scope per key type**

| Key type | Pushed events |
|----------|----------------|
| `private` | `message.created` for the bound private room (respects `@mention` targets; sender key excluded) |
| `delegate` | `message.created` DMs where the owner is a party **and** the peer ACL is enabled (own API sends are not echoed back to the same key) |
| `bot` | Inbound bot DMs + inbound thread messages (+ private room if the bot key is bound). Bot’s own sends are not echoed |

Also pushed: `message.interaction` (HTML button clicks) to the **sending** key — same payload as the webhook.

Outbound streams (see **Streaming outbound**): `message.started`, `message.delta`, `message.completed` for peers watching the conversation; webhooks still receive a single final `message.created`.

**On connect**

```json
{ "type": "hello", "key_id": "…", "key_type": "private|delegate|bot", "ts": 0 }
```

**Server → client keepalive:** `{ "type": "ping" }` — reply optional; client may send `{ "type": "ping" }` → `{ "type": "pong" }`.

**Example event (`message.created`)**

```json
{
  "type": "message.created",
  "channel": "dm",
  "key_id": "…",
  "key_type": "bot",
  "bot_id": "…",
  "peer_user_id": "…",
  "message": {
    "id": "…",
    "sender_id": "…",
    "recipient_id": "…",
    "body": "hello",
    "content_type": "text",
    "created_at": 0
  }
}
```

`channel` is one of `private_room` | `dm` | `thread`.

```bash
# Node example (ws)
# npm i ws
node -e '
const WebSocket = require("ws");
const key = process.env.WUSPER_API_KEY;
const ws = new WebSocket("wss://chat.wusper.com/api/v1/ws", {
  headers: { Authorization: "Bearer " + key }
});
ws.on("message", (d) => console.log(String(d)));
'
```

```bash
# Query-token (e.g. websocat / browser)
websocat "wss://chat.wusper.com/api/v1/ws?access_token=$WUSPER_API_KEY"
```

Traefik/Coolify must allow WebSocket upgrade for `/api/v1/ws` (same as `/api/ws`).

---

---

## Streaming outbound (progressive messages)

Bots / API clients can **stream** a message into the UI (one bubble that grows) instead of posting only when finished. Works for **all three key types** on their normal send channels.

### Switch: `stream: true` on the usual send POST

Streaming is a **request flag**, not a separate mandatory API surface:

| `stream` | Behavior |
|----------|----------|
| **absent / `false`** | One-shot send (unchanged): body stored + delivered immediately |
| **`true`** | Start a progressive message (`streaming: true`); then send chunks + complete |

Preferred start endpoints (same as one-shot, plus `"stream": true`):

| Channel | Start with `stream: true` |
|---------|---------------------------|
| private / bot-bound room | `POST /api/v1/private/messages` |
| delegate DM | `POST /api/v1/messages` `{ "to", "stream": true, … }` |
| bot DM | `POST /api/v1/dm/messages` |
| bot thread | `POST /api/v1/threads/:id/messages` |

Alias paths (same behavior): `…/messages/stream` still work.

After start, shared:

- `POST /api/v1/messages/:id/chunks` — `{ "text"|"delta"|"body": "…", "mode": "append"|"replace" }` or `{ "html": "…", "mode": "replace" }`
- `POST /api/v1/messages/:id/complete` — optional final `{ "body" }` / `{ "html" }`; omit to keep last chunk
- `POST /api/v1/messages/:id/abort` — cancel in-flight stream

**Defaults:** text chunks **append**; HTML chunks **replace**. Max text **4000**; HTML **48KB**. Max **500** chunks / message. Chunk rate **180/min** per key. `client_message_id` / `Idempotency-Key` on **start** dedupes the whole stream. Completing twice → `deduped: true`. FCM fires **once** on complete.

**Empty placeholder UX:** empty start is **not** shown in session UI; bubble appears on first non-empty delta (or complete with body). Completing still-empty → `cancelled: true`.

### HTML while streaming

HTML **is accepted** on start / chunks / complete (`content_type: html`) and renders normally.

**HTML components (button interact / `report_actions`) are not applicable while streaming is on** — including after `complete`. The message is stored with `interact: false`. One-shot `POST …/messages` without `stream:true` keeps normal HTML interact behavior.

When HTML is used on a stream, the API response includes:

```json
{
  "warning": "html_components_unavailable_while_streaming",
  "warning_message": "HTML 組件（按鈕互動／report_actions）在流式傳輸開啟時不適用；HTML 內容仍會照常儲存與顯示。",
  "warning_message_en": "HTML components (button interact / report_actions) are not available while streaming is on; HTML content is still accepted and rendered as usual.",
  "stream": true,
  "message": { "interact": false, "content_type": "html", "streaming": true }
}
```

`POST /api/interactions` on a streamed message returns `400` with `status: "html_components_unavailable_while_streaming"`.

### Realtime events

Session UI (`/api/ws`): `dm` / `private_room` / `thread` with `stream: "start"|"delta"|"complete"` and `message.streaming`.

API WS (`/api/v1/ws`): `message.started` / `message.delta` / `message.completed`.

Webhooks: **only** final `message.created` on complete.

### Example (bot DM — preferred `stream: true`)

```bash
# 1) start on the normal send path
MSG=$(curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"to\":\"$PEER_ID\",\"body\":\"\",\"stream\":true,\"client_message_id\":\"stream-1\"}" \
  https://chat.wusper.com/api/v1/dm/messages)
ID=$(printf '%s' "$MSG" | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' | head -1)

# 2) chunks
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"delta":"Hello"}' \
  "https://chat.wusper.com/api/v1/messages/$ID/chunks"
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"delta":" world"}' \
  "https://chat.wusper.com/api/v1/messages/$ID/chunks"

# 3) complete
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{}' \
  "https://chat.wusper.com/api/v1/messages/$ID/complete"
```

Default (no `stream` / `stream:false`) remains one-shot `POST …/messages`.


## Common

### GET `/api/v1/me`

Works for all key types.

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  https://chat.wusper.com/api/v1/me
```

---


## Message list order (`order` / `sort`)

These GETs accept **`order`** (preferred) or **`sort`**:

| Value | Aliases | Behavior |
|-------|---------|----------|
| `oldest` (**default**) | `asc` | Ascending by `created_at`; returns the **oldest** `limit` rows |
| `newest` | `desc`, `latest`, `new` | Descending by `created_at`; returns the **newest** `limit` rows |

Applies to:

- `GET /api/v1/private/messages`
- `GET /api/v1/messages` (delegate)
- `GET /api/v1/dm/messages` (bot)
- `GET /api/v1/threads/:id/messages` (bot)

`limit` default **100**, max **200**. Response includes echo field `"order": "oldest"|"newest"`.  
There is no cursor yet: change `order` / `limit` to choose the window. For live inbound use the WebSocket above (or webhooks).


## Private room key (`type: private`)

Bound to one private room (also usable by a bot key that is bound to a room).

### GET `/api/v1/private/messages?limit=`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  'https://chat.wusper.com/api/v1/private/messages?limit=50&order=newest'
```

### POST `/api/v1/private/messages`

```bash
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"body":"hello from private API"}' \
  https://chat.wusper.com/api/v1/private/messages
```

---

## Delegate key (`type: delegate`)

Acts as the owner account. Each friend DM must be **ACL-enabled** in 設置 → API → manage.

### GET `/api/v1/friends`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  https://chat.wusper.com/api/v1/friends
```

### GET `/api/v1/messages?with=<userId>`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  "https://chat.wusper.com/api/v1/messages?with=$FRIEND_ID&order=newest"
```

### POST `/api/v1/messages`

```bash
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"to\":\"$FRIEND_ID\",\"body\":\"hello via delegate\"}" \
  https://chat.wusper.com/api/v1/messages
```

---

## Bot key (`type: bot`)

Independent bot identity. DMs require peer invite / share-code claim. Group-of-3 threads via 設置.

### GET `/api/v1/bot/me`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  https://chat.wusper.com/api/v1/bot/me
```

### GET `/api/v1/dm/peers`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  https://chat.wusper.com/api/v1/dm/peers
```

### GET|POST `/api/v1/dm/messages`

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  "https://chat.wusper.com/api/v1/dm/messages?with=$PEER_ID&order=newest"

curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"to\":\"$PEER_ID\",\"body\":\"hi from bot\"}" \
  https://chat.wusper.com/api/v1/dm/messages
```

### Threads

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  https://chat.wusper.com/api/v1/threads

curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"body":"hello thread"}' \
  "https://chat.wusper.com/api/v1/threads/$THREAD_ID/messages?order=newest&limit=50"
```

### Webhooks


## Delete records

Soft-delete / per-user hide. Friendship is **not** removed when clearing a conversation.

| Scope | Effect |
| --- | --- |
| `me` | Hide for the acting identity only (`message_hides`). Peer keeps their copy. |
| `everyone` | Tombstone for all viewers (`deleted_at`). **DM / thread:** only the **sender**. **Private room:** sender key or bound private key. UI shows an empty “deleted” placeholder. |

### DELETE `/api/v1/messages/:id`

Bearer: **delegate** (DM), **bot** (DM or thread), **private** / **bot** (private room).

```bash
curl -sS -X DELETE "https://chat.wusper.com/api/v1/messages/$MSG_ID" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"scope":"me","channel":"dm"}'
```

Body:

- `scope`: `me` | `everyone` (default `me`)
- `channel`: `dm` | `private_room` | `thread` (optional; auto-detected from id)

Response: `{ ok, scope, message_id, deleted_at?, already? }`

Realtime: `message.deleted` on session `/api/ws` and API `/api/v1/ws` (everyone → peers; me → actor only).

### DELETE `/api/v1/conversations`

Clears history **for the acting identity** and hides the thread from their list until a newer visible message arrives. Does **not** unfriend.

```bash
# Delegate / bot DM
curl -sS -X DELETE "https://chat.wusper.com/api/v1/conversations" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"kind":"dm","with":"$PEER_ID"}'

# Private key room
curl -sS -X DELETE "https://chat.wusper.com/api/v1/conversations" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"kind":"private_room","room_id":"$ROOM_ID"}'

# Bot thread
curl -sS -X DELETE "https://chat.wusper.com/api/v1/conversations" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"kind":"thread","thread_id":"$THREAD_ID"}'
```

Response: `{ ok, kind, peer_id, cleared_before, list_hidden }`. Event: `conversation.deleted`.

Session cookie UI mirrors these as `DELETE /api/messages/:id` and `DELETE /api/conversations`.


Optional `webhook_url` on private / bot keys (HTTP push). Prefer **Realtime WebSocket** above for low-latency inbound; webhooks still work. Server POSTs JSON `message.created` with header:

`X-Wusper-Signature: sha256=<hmac>`

---

## Node SDK snippet

```js
import { WusperChatClient } from '@wusper/chat-sdk';

const client = new WusperChatClient({ apiKey: process.env.WUSPER_API_KEY });
console.log(await client.me());
await client.sendPrivateMessage('hello');
```

---

## HTML components (`content_type: html`)

All three key types (and bot threads) can send an **HTML component** message that renders as interactive UI in chat (buttons, links, layout), isolated in a sandboxed Shadow DOM.

### Request shapes (any of these)

Plain text (unchanged):

```json
{ "body": "hello" }
```

HTML component:

```json
{ "html": "<button id=\"ok\" data-action=\"confirm\">Confirm</button>" }
```

```json
{ "body": "<button id=\"ok\">Confirm</button>", "format": "html" }
```

```json
{ "type": "html_component", "html": "<div><button id=\"a\">A</button><button id=\"b\">B</button></div>" }
```

- Stored as `content_type: "html"` (text messages stay `"text"`).
- Server allowlist-sanitizes HTML (no `script` / `on*` / `javascript:`). Max **48KB**.
- Allowed interactive tags include `<button>`, safe `<a href="https://…">`, plus basic layout tags.

### Interaction reporting (`interact` / `report_actions`)

- **Default for HTML: `interact: true`** — user clicks on `<button>` / `[data-action]` are reported back to the **sending API key** (webhook `message.interaction` + pollable via `GET /api/v1/interactions`).
- Set **`"interact": false`** (alias **`report_actions`: false**) to render the component **without** click callbacks.
- Plain text messages never report interactions.
- **Streaming (`stream: true` or `…/messages/stream`):** HTML is still accepted and rendered, but **interact is always forced off** (even after complete). The send response includes `warning: "html_components_unavailable_while_streaming"`. Use one-shot send (no stream flag) when you need button callbacks.

```json
{ "html": "<button id=\"ok\">OK</button>", "interact": true }
```

```json
{ "html": "<button id=\"ok\">OK</button>", "interact": false }
```

### Click → callback to **your** API

When a user clicks a `<button>` (or `[data-action]`) in the component:

1. Chat UI `POST /api/interactions` (session).
2. Server stores the event and POSTs webhook event **`message.interaction`** to the **sending API key’s** webhook (same URL/secret as `message.created`).
3. You can also poll: `GET /api/v1/interactions?since=&limit=`.

Webhook / poll payload (abbrev.):

```json
{
  "type": "message.interaction",
  "key_id": "…",
  "interaction": {
    "id": "…",
    "type": "click",
    "message_id": "…",
    "user_id": "…",
    "element": { "tag": "button", "id": "ok", "label": "Confirm", "dataset": { "action": "confirm" } },
    "created_at": 0
  },
  "message": { "id": "…", "content_type": "html", "body": "…" }
}
```

### Examples

Private:

```bash
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"html":"<button id=\"go\" data-action=\"go\">Go</button>"}' \
  https://chat.wusper.com/api/v1/private/messages
```

Delegate:

```bash
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"to\":\"$FRIEND_ID\",\"html\":\"<button id=\\"yes\\">Yes</button>\"}" \
  https://chat.wusper.com/api/v1/messages
```

Bot DM:

```bash
curl -sS -X POST -H "Authorization: Bearer $WUSPER_API_KEY" \
  -H 'Content-Type: application/json' \
  -d "{\"to\":\"$PEER_ID\",\"html\":\"<button id=\\"ack\\">Ack</button>\"}" \
  https://chat.wusper.com/api/v1/dm/messages
```

Poll interactions:

```bash
curl -sS -H "Authorization: Bearer $WUSPER_API_KEY" \
  'https://chat.wusper.com/api/v1/interactions?limit=20'
```
