# Chamade — Complete Documentation > Voice and chat gateway for AI agents. Chamade joins meetings, phone calls, and DMs on 10+ platforms and runs the speech pipeline for you using your own STT/TTS provider key (BYOK) — or hands you raw PCM on a WebSocket if you'd rather run the speech layer yourself. Chamade is a **voice and chat gateway**: one uniform API to join meetings, phone calls, and DMs across Discord, Teams, Meet, SIP, Telegram, WhatsApp, Slack, NC Talk, and Zoom. Two voice modes, pick one: **Voice — hosted STT/TTS (recommended, BYOK)**: add your ElevenLabs / Deepgram key once in /dashboard → Voice providers; Chamade runs the pipeline server-side (free — you pay your provider, not Chamade). `call_transcript` events fire, `chamade_call_say` speaks, `chamade_call_stop_speaking` interrupts. **Voice — raw PCM WebSocket (alternative, full control)**: if you already have a voice pipeline you want to keep (OpenAI Realtime, LiveKit Agents, Pipecat, Deepgram Voice Agent, manual Whisper / ElevenLabs / Cartesia cascade, …), Chamade exposes the call's raw PCM audio bidirectionally. You run STT/TTS locally, Chamade just moves the bytes. For text conversations (DMs, group chats, in-call chat, inbox), Chamade is fully plug-and-play via MCP tools (`chamade_inbox`, `chamade_dm_chat`, `chamade_call_chat`) — no raw audio involved. Every API endpoint (except account creation) accepts both session cookies and API keys — the dashboard and your bot use the exact same API. Account management (API keys, bot tokens, platform connections, SIP) is all available programmatically. Works with any backend that speaks HTTP + WebSocket. **IMPORTANT:** This is a non-production instance. Set `CHAMADE_URL=https://dev.chamade.io` in your MCP env config. The default is `https://chamade.io`. --- ## Quick Start ### 1. Get an API Key Sign up at https://dev.chamade.io/dashboard, then create an API key. It starts with `chmd_` and is shown only once — store it securely. ### 2. Choose Your Integration **MCP — hosted Streamable HTTP (recommended for Claude Desktop, Claude Code, Cursor, Windsurf, any client that supports the spec's Streamable HTTP transport):** ```json { "mcpServers": { "chamade": { "type": "http", "url": "https://mcp.dev.chamade.io/mcp/", "headers": { "Authorization": "Bearer chmd_..." } } } } ``` For Claude Code specifically, real-time push events (new DMs, ringing calls, call state changes) require TWO opt-ins: adding `?stateful` to the MCP URL in `.mcp.json`, and launching Claude Code with the channel flag. Without them, the tools still work in polling mode via `chamade_inbox` long-polling — recommended as the default because it is restart-proof. Full details and the exact URL + command are in the [Channel Mode](#channel-mode-real-time-push) section below. **MCP — stdio shim for legacy clients** (OpenClaw, older Windsurf, anything that only speaks stdio): The `@chamade/mcp-server@3` npm package is a thin stdio → HTTP bridge around the hosted endpoint. Same tools, same push events, same latency — it's just a transport adapter. ```json { "mcpServers": { "chamade": { "command": "npx", "args": ["-y", "@chamade/mcp-server@3"], "env": { "CHAMADE_API_KEY": "chmd_...", "CHAMADE_URL": "https://dev.chamade.io" } } } } ``` **REST API** — direct HTTP calls: ```bash curl -X POST https://dev.chamade.io/api/call \ -H "X-API-Key: chmd_..." \ -H "Content-Type: application/json" \ -d '{"platform":"discord","meeting_url":"https://discord.com/channels/..."}' ``` ### 3. Connect Your Platforms Some platforms require setup in the Dashboard before use: | Platform | Setup needed | |----------|-------------| | Discord | Works out of the box (shared bot) or add your own bot | | Microsoft Teams | Connect Microsoft account (OAuth) | | Google Meet | Connect Google account (OAuth) | | Zoom | Connect Zoom account (OAuth, beta — requires invite) | | Telegram | Works out of the box or add your own bot | | WhatsApp | No setup — invite link | | Slack | Install the Slack app | | Nextcloud Talk | Install addon + connect | | SIP / Phone | Connect your SIP trunk and add your DIDs | --- ## Supported Platforms | Platform | Voice | Chat | Files | Setup | |----------|-------|------|-------|-------| | Discord | audio_in, audio_out | read, write, typing | ✅ send + receive | Invite Chamade bot or bring your own | | Microsoft Teams | audio_in, audio_out | read, write, typing | gated (code-ready, waiting AppSource cert) | Install Teams app + connect Microsoft account | | Google Meet | audio_in | read, write | gated (code-ready, waiting Google OAuth verif) | Connect Google account (Media API in Dev Preview, see notes) | | Zoom | audio_in, audio_out | read, write | — | Connect Zoom account (beta) | | Telegram | — | read, write, typing | ✅ send + receive | DM the Chamade bot or bring your own | | WhatsApp | — | read, write | ✅ send + receive (images, audio, video, voice notes, docs, stickers) | Share a link — no setup needed | | Slack | — | read, write | ✅ send + receive | Install Chamade app or bring your own | | Nextcloud Talk | audio_in, audio_out | read, write, typing | ✅ send + receive (inbound requires addon ≥ 2.4.0) | Connect via dashboard | | SIP / Phone | audio_in, audio_out | — | — | Bring your own SIP trunk + DIDs (BYOT only) | **Capabilities:** - **audio_in** — Chamade streams raw PCM *from* the platform to your agent (you receive the humans' voice as bytes on the call's audio WebSocket, then run your own STT on them) - **audio_out** — Chamade accepts raw PCM *into* the platform from your agent (you generate voice via your own TTS and send the bytes to the call's audio WebSocket) - **read** — receive text/chat messages from the channel (JSON events on the text WebSocket, or polling REST) - **write** — send text/chat messages to the channel (`POST /api/call/{id}/chat` or `chamade_call_chat` MCP tool) - **typing** — typing indicator supported - **files** — send and receive file attachments (25 MB cap). Inbound attachments come back as Chamade-signed URLs in `/api/inbox` responses and push events. Outbound: pass `attachments: [{file_id} | {url} | {bytes_b64, name, mime}]` on `POST /api/dm/chat` or `POST /api/call/{id}/chat`. See the "Files & attachments" section below for the full per-platform matrix. If **audio_out** is not available on a platform (e.g. Google Meet where voice injection is blocked), use `POST /api/call/{id}/chat` to send text messages instead. **Audio format** (same for every platform): PCM s16le mono, 20ms frames. The native sample rate differs by platform (48 kHz for Discord/Meet/NC Talk, 16 kHz for Teams/Zoom/Telegram, 8 kHz for SIP). Your agent can negotiate a target rate via `audio_config` — Chamade resamples transparently. See the Audio WebSocket section below for the protocol. ### Message Formatting and Limits Each platform has its own message length limit and formatting support. Messages exceeding the platform limit will be rejected with an error — split long messages yourself at natural boundaries. | Platform | Max Length | Formatting | |----------|-----------|------------| | Discord | 2,000 chars | Markdown (bold, italic, strikethrough, code, headers, links) | | Telegram | 4,096 chars | Markdown (`*bold*`, `_italic_`, `` `code` ``, ` ```blocks``` `, `[links](url)`) | | Teams | ~28,000 chars (40 KB UTF-16) | Markdown | | WhatsApp | 1,600 chars | Basic (`*bold*`, `_italic_`, `~strikethrough~`, `` `monospace` ``). Needs spaces around markers | | Slack | 3,000 chars/block (40,000 total) | mrkdwn (`*bold*`, `_italic_`, `~strikethrough~`, `` `code` ``) | | NC Talk | 32,000 chars | Markdown | **Common subset** (works on all platforms): `*bold*`, `_italic_`, `` `code` ``, ` ```code blocks``` `. Prefer this for cross-platform messages. --- ## API Reference ### Authentication All API calls require the `X-API-Key` header: ``` X-API-Key: chmd_your_key_here ``` API keys are created in the Dashboard. They start with `chmd_` and are shown only once at creation. ### Create Call ``` POST /api/call ``` Join a meeting or initiate a call. Returns the call ID and initial state. **Request body:** ```json { "platform": "discord", "meeting_url": "https://discord.com/channels/...", "agent_name": "AI Agent" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `platform` | string | Yes | One of: `discord`, `teams`, `meet`, `zoom`, `telegram`, `nctalk`, `sip`, `whatsapp` | | `meeting_url` | string | Depends | Meeting URL or SIP URI. Required for most platforms. | | `agent_name` | string | No | Display name for the agent in the meeting. Default: "AI Agent" | **Response:** ```json { "call_id": "abc123", "room_id": "...", "participant_id": "...", "text_ws_url": "wss://...", "state": "connecting", "capabilities": ["audio_in", "audio_out", "read", "write"], "platform": "discord", "meeting_url": "https://discord.com/channels/...", "agent_name": "AI Agent", "audio": { "sample_rate": 48000, "format": "pcm_s16le", "channels": 1, "frame_duration_ms": 20, "frame_bytes": 1920 } } ``` ### Get Call Status ``` GET /api/call/{call_id}?since=0 ``` Get the current state of a call, including transcript lines. The `since` parameter enables a delta pattern: only transcript lines after that index are returned, so you can poll efficiently without re-reading the entire transcript. **Response:** ```json { "call_id": "abc123", "room_id": "...", "participant_id": "...", "text_ws_url": "wss://...", "state": "active", "capabilities": ["audio_in", "audio_out", "read", "write"], "audio": {"sample_rate": 48000, "format": "pcm_s16le", "channels": 1, ...}, "platform": "discord", "meeting_url": "https://discord.com/channels/...", "agent_name": "AI Agent", "transcript": ["[Alice] Hello everyone", "[Bob] Hi Alice"], "transcript_length": 42, "direction": "outbound", "caller": "", "created_at": "2026-03-13T10:00:00Z", "ended_at": null } ``` **Delta pattern:** On the first call, use `since=0` to get all transcript lines. Then pass `since={transcript_length}` from the response to get only new lines on subsequent polls. ### Speak (hosted TTS) — [BYOK] ``` POST /api/call/{call_id}/say ``` > **[BYOK hosted TTS]** Requires a TTS preset in /dashboard → Voice providers. Returns 400 without one — fall back to BYO audio: synthesize locally and push PCM on the call's audio WebSocket (see "Direct Audio Streaming" below). Speak text aloud in the meeting via Chamade's hosted TTS. Only meaningful on platforms with the `audio_out` capability. **Request body:** ```json {"text": "Hello, I am your AI assistant."} ``` ### Send Chat ``` POST /api/call/{call_id}/chat ``` Send a text chat message in the meeting. Works on platforms with the `write` capability. **Request body:** ```json { "text": "Here is the link: https://example.com", "sender_name": "AI Assistant" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `text` | string | Yes | Message text (1–10,000 characters) | | `sender_name` | string | No | Display name for the sender in meeting chat cards. Default: empty. | ### Accept Inbound Call ``` POST /api/call/{call_id}/accept ``` Accept a ringing inbound call (SIP, etc.). Changes the call state from `"ringing"` to `"active"`. ### Refuse Inbound Call ``` POST /api/call/{call_id}/refuse ``` Refuse/reject a ringing inbound call. Changes the call state from `"ringing"` to `"refused"`. ### Hang Up ``` DELETE /api/call/{call_id} ``` End the call and leave the meeting. The call state changes to `"ended"`. ### List Active Calls ``` GET /api/calls ``` Returns a list of all active calls for the authenticated user. ### Typing Indicator (in-call) ``` POST /api/call/{call_id}/typing ``` Send a typing indicator in the meeting chat. Supported on platforms with the `typing` capability. ### Account Status ``` GET /api/account ``` Bootstrap call for any agent. Returns: - `plan` — your current plan (in early access this is informational only) - `features` — global gateway availability (`transport`, `audio_in`, `audio_out`, `text_chat`, `typing_indicators`, `files` are all `ready`; `hosted_stt` and `hosted_tts` are `ready` when the user has an enabled preset in /dashboard → Voice providers, `byok` otherwise — add a key and Chamade runs the pipeline on its side for free) - `features_note` — plain-English explanation of the BYOK hosted STT/TTS model - `platforms` — per-platform `{status, capabilities, files}` for every supported platform. `status` tells you if the platform is `ok`, `not_configured`, or in error. `capabilities` tells you what the platform exposes (`audio_in`, `audio_out`, `read`, `write`, `typing`, `files`). The top-level `files` bool on each platform is the truth for "does send + receive work end-to-end today?" (`true` on Discord, Telegram, Slack, WhatsApp, NC Talk; `false` on Teams, Meet, SIP, Zoom until their gates flip) - `concurrent_calls` — active call count vs limit - `identities` — which handle on each platform is your agent vs the human operator (critical for self-chat disambiguation; see the DM workflow section) - `last_message_cursor` — starting cursor for `chamade_inbox` delta mode Call this once on cold start to bootstrap your agent. --- ## WebSocket Stream ``` WS /api/call/{call_id}/stream?api_key=chmd_... ``` Real-time bidirectional stream for receiving transcripts and chat, and sending speech and chat messages. Alternative to polling `GET /api/call/{id}`. **Incoming messages (server to client):** ```json {"type": "call_transcript", "speaker": "Alice", "text": "Hello"} {"type": "call_chat", "sender": "Bob", "text": "Hi"} {"type": "call_state", "state": "active"} {"type": "call_error", "message": "..."} ``` **Outgoing messages (client to server):** ```json {"type": "say", "text": "Hello everyone"} {"type": "send_chat", "text": "Check this link"} ``` ### Direct Audio Streaming — the BYO-audio path (alternative to hosted STT/TTS) The same call WebSocket exposes the room's raw PCM audio bidirectionally. You receive what the humans say (binary frames) and push what your agent says back (binary frames). In this mode Chamade is pure transport — you run STT and TTS. Bring your own voice models. Popular choices: - **OpenAI Realtime API** (24 kHz WebSocket, handles STT+LLM+TTS in one stream) - **LiveKit Agents SDK** (Python, plug Chamade audio as a track source) - **Pipecat** (Daily's Python voice AI framework) - **Deepgram Voice Agent** (integrated STT+LLM+TTS) - **Roll your own cascade** (Whisper / Deepgram STT → GPT/Claude → ElevenLabs / Cartesia TTS) The `audio` object in the `POST /api/call` response tells you the native sample rate of the room. Sending audio at that rate avoids resampling and gives the best quality. | Platform | Native rate | |----------|-------------| | Discord, Meet, NC Talk | 48,000 Hz | | Teams, Zoom, Telegram | 16,000 Hz | | SIP | 8,000 Hz | **Format:** raw PCM, signed 16-bit little-endian, mono, at the rate above (or your declared rate via `audio_config`). Internal frame cadence is 20 ms. **Option A — Binary frames (recommended):** Send raw PCM s16le mono as binary WebSocket frames. Zero overhead, no JSON encoding. Each frame may contain any number of samples (Chamade buffers and re-aligns to 20 ms internally). Hard cap: 1 MB per frame (~10 s of audio at 48 kHz). **Option B — Base64 JSON:** Compatible with OpenAI Realtime and similar JSON-based streaming patterns. Slightly higher overhead due to base64 encoding + JSON parsing. ```json {"type": "audio", "audio": "", "sample_rate": 24000} ``` **Audio config (optional):** Send this to discover the native sample rate, declare your own rate (Chamade will resample), or opt into receiving the meeting's audio mix. ```json // Query native rate {"type": "audio_config"} // Response {"type": "audio_config", "sample_rate": 48000, "native_sample_rate": 48000, "resampling": false, "format": "pcm_s16le", "channels": 1, "frame_duration_ms": 20, "frame_bytes": 1920, "ready": true} // Override with your rate (Chamade resamples automatically) {"type": "audio_config", "sample_rate": 24000, "receive": true} // Response {"type": "audio_config", "sample_rate": 24000, "native_sample_rate": 48000, "resampling": true, "format": "pcm_s16le", "channels": 1, "frame_duration_ms": 20, "frame_bytes": 960, "ready": true} ``` **Best performance:** Send binary frames at the native sample rate (from the `audio` field in the call response). No config message needed, no resampling, lowest latency. #### Receiving the meeting audio (mix-minus) To run your own STT (Whisper, Deepgram, etc.) instead of Chamade's, opt into receiving the meeting audio with `audio_config` + `"receive": true`. - You receive raw PCM s16le mono as **binary WebSocket frames**, at your declared `sample_rate` (Chamade resamples for you), at a 20 ms cadence (~50 frames/second). - It's **mix-minus**: the audio you injected via Option A/B is excluded from the return stream — you don't hear yourself echo. Only the other participants. - The same WebSocket continues to deliver `call_transcript`, `call_chat`, and `call_state` events as JSON text frames in parallel. You can mix Chamade's STT with your own. #### Handling disconnects The bridge between Chamade and the meeting platform can drop (Maquisard restart, network blip, platform-side disconnect). When that happens, all subscribed agent WebSockets receive: ```json {"type": "call_error", "code": "bridge_disconnected", "message": "Bridge connection lost. Use chamade_call_join with the same meeting_url to reconnect."} ``` To recover, call `POST /api/call` again with the same `meeting_url`. A new `call_id` is issued and a new WebSocket can be opened. Previous transcript lines are not carried over — the room is fresh. #### Python example Minimal client that opens the WebSocket, declares 24 kHz, sends a chunk of PCM (your TTS output), and receives the meeting audio for a custom STT pipeline. ```python import asyncio import json import httpx import websockets API_KEY = "chmd_..." # from chamade.io/dashboard async def main(): # 1. Create the call with transcripts disabled (BYO-STT) async with httpx.AsyncClient() as http: r = await http.post( "https://chamade.io/api/call/", headers={"X-API-Key": API_KEY, "Content-Type": "application/json"}, json={ "platform": "discord", "meeting_url": "https://discord.com/channels/.../...", "transcripts": False, # BYO-STT — Chamade pays no STT/TTS }, ) r.raise_for_status() call = r.json() call_id = call["call_id"] # 2. Open the WebSocket url = f"wss://chamade.io/api/call/{call_id}/stream?api_key={API_KEY}" async with websockets.connect(url) as ws: # 3. Declare your sample rate + opt into receiving audio await ws.send(json.dumps({ "type": "audio_config", "sample_rate": 24000, "receive": True, })) async def reader(): async for msg in ws: if isinstance(msg, bytes): # Raw PCM s16le mono @ 24 kHz, 20 ms frames, mix-minus. # Feed this to your STT (Whisper, Deepgram, ...). print(f"audio frame: {len(msg)} bytes") else: event = json.loads(msg) etype = event.get("type") if etype == "audio_config": print(f"audio_config: {event}") elif etype == "call_transcript": print(f'{event["speaker"]} → {event["text"]}') elif etype == "call_chat": print(f'chat <{event["sender"]}>: {event["text"]}') elif etype == "call_error" and event.get("code") == "bridge_disconnected": print("Bridge dropped — re-create the call.") return async def writer(): # Your TTS would produce 24 kHz s16le PCM here. Stub: 1 s of silence. pcm_one_second = b"\x00\x00" * 24000 # 24000 samples × 2 bytes await ws.send(pcm_one_second) # binary frame, sent as-is await asyncio.sleep(60) # keep the call alive for the reader await asyncio.gather(reader(), writer()) asyncio.run(main()) ``` > **MCP cannot stream audio — this is architectural.** MCP (JSON-RPC 2.0 over stdio / Streamable HTTP) is a control plane, not a data plane. There is no primitive in the spec for continuous binary streams, no backpressure, no flow control. Every voice infrastructure in the ecosystem keeps audio on a native WebSocket (or WebRTC) and uses text protocols (REST, MCP, gRPC) only for control. Chamade follows the same pattern: MCP tools drive the call (`chamade_call_join`, `chamade_call_chat`, etc.), and the raw PCM flows over the separate call WebSocket described above. Your agent's host code (not the LLM itself, which can't do socket I/O) connects to both and pipes bytes between the call WS and your chosen STT/TTS stack. --- ## Inbox API (DMs) The Inbox API lets your agent read and respond to direct messages from platforms like Discord, Telegram, WhatsApp, Teams, and Slack. ### List Conversations ``` GET /api/inbox?platform=discord&limit=50 ``` Returns a list of DM conversations. Filter by platform and limit results. | Param | Type | Required | Description | |-------|------|----------|-------------| | `platform` | string | No | Filter by platform (discord, telegram, teams, whatsapp, slack, nctalk) — in detail mode, returns the active DM on that platform with its full message history | | `last_message_cursor` | string | No | Delta mode: return only conversations with new messages since this cursor. Accepts either `|` (from prior calls) or a plain ISO timestamp | | `wait` | int | No | Delta mode: long-poll up to N seconds for new messages (server-capped at 55s). Requires `last_message_cursor` | | `limit` | int | No | Max conversations to return (default 50) | | `status` | string | No | Filter by status (default "active") | | `offset` | int | No | Pagination offset (default 0) | **WhatsApp-specific field: `whatsapp_window`** Every WhatsApp conversation in the response carries an extra `whatsapp_window` block so you can check the state of the 24-hour customer service window BEFORE sending a message. Use it to anticipate whether your next `chamade_dm_chat` will land immediately (200 delivered) or get queued behind a re-engagement template (202 queued). ```json { "platform": "whatsapp", "remote_name": "+32...", "whatsapp_window": { "open": false, "last_inbound_at": "2026-04-10T04:02:15+00:00", "pending_count": 2 }, ... } ``` | Field | Type | Description | |-------|------|-------------| | `open` | bool | `true` if the current time is within 24h of `last_inbound_at` — plain text sends will go through immediately | | `last_inbound_at` | string \| null | ISO timestamp of the user's last inbound message (null if they have never messaged, or if this is a freshly-linked conversation) | | `pending_count` | int | Number of outbound messages currently queued behind the re-engagement template, waiting for the user to reply | Non-WhatsApp conversations (Discord, Teams, etc.) do NOT carry this field. Only check for `whatsapp_window` on conversations where `platform === "whatsapp"`. ### Get Conversation ``` GET /api/inbox/{conversation_id}?limit=50 ``` Get messages from a specific conversation. Use `limit` to control how many messages are returned. ### Send Message ``` POST /api/dm/chat ``` **Request body:** ```json { "platform": "discord", "text": "Thanks for reaching out!" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `platform` | string | Yes | Platform (discord, teams, telegram, whatsapp, slack, nctalk) | | `text` | string | Yes | Message text (1–10,000 characters) | | `keep_typing` | boolean | No | Re-activate typing indicator after sending. Use when sending a quick acknowledgment before a long task. Default: false | **Response — two delivery outcomes:** Chamade returns different HTTP status codes depending on whether delivery was synchronous or had to be queued: - **`200 OK` — delivered synchronously** ```json {"status": "delivered", "message_id": "msg_...", "delivery": "sync"} ``` The message is on the platform. You can move on. - **`202 Accepted` — queued (WhatsApp only, outside the 24h window)** ```json { "status": "queued", "message_id": "msg_...", "reason": "outside_24h_window", "template_sent": true, "queue_position": 1 } ``` WhatsApp only allows free-form messages within 24 hours of the user's last reply. Outside that window, Chamade stores your message and fires a pre-approved re-engagement template (`agent_followup`) so the user knows something's waiting. When they reply, all pending messages flush automatically in chronological order. You'll receive a `dm_delivered` event on the inbox WebSocket the moment the flush happens — listing the `delivered_ids` that just went out. Immediately after, you'll receive the user's reply as a normal `dm_chat` event. No retry needed on your side. Queue behaviour for multiple pending messages: - The *first* message outside the window triggers one re-engagement template. - Subsequent messages (messages 2, 3, ...) just join the queue — no additional templates (keeps WhatsApp quality rating healthy). - Pending messages older than **7 days** are automatically dropped. ### Typing Indicator (DM) ``` POST /api/dm/typing ``` **Request body:** ```json {"platform": "discord"} ``` ### Reply Timeout (60 seconds) After a user sends a DM, you have **60 seconds** to reply via `POST /api/dm/chat`. If no reply is sent in time, the user sees a timeout error message. If a task will take longer than a few seconds: 1. Send a short acknowledgment immediately via `POST /api/dm/chat` with `keep_typing: true` (e.g. "Looking into it..."). The `keep_typing` flag re-activates the typing indicator automatically after sending. 2. Do your work, then send the full answer via `POST /api/dm/chat` (without `keep_typing`). 3. If your work takes more than 60 seconds, call `POST /api/dm/typing` every ~55 seconds to keep the typing indicator alive. --- ## Files & Attachments Chamade relays file attachments in both directions on DMs and meeting chat. Inbound, every attachment is fetched eagerly and re-served from a Chamade-signed URL — your agent never touches platform-native URLs or bearer tokens. Outbound, you upload to Chamade once and reference the `file_id` any number of times. **Per-platform matrix:** | Platform | Send | Receive | Notes | |----------|------|---------|-------| | Discord | ✅ | ✅ | 25 MB limit (non-Nitro). CDN URLs signed ~24 h, refetcher = plain HTTPS | | Telegram | ✅ | ✅ | `file_id` persists; refetch via `getFile` with BYOB or shared bot token | | Slack | ✅ | ✅ | Uses modern `files.getUploadURLExternal` (legacy `files.upload` retired 2025-11-12) | | WhatsApp | ✅ | ✅ | Images, audio, video, voice notes (raw `audio/ogg`, auto-STT is V2), documents, stickers. Meta: no caption on audio | | NC Talk | ✅ | ✅ (addon ≥ 2.4.0) | WebDAV upload + OCS share `shareType=10`. Inbound needs `chamade_talk` at v2.4.0+ | | Microsoft Teams | code-ready, gated | code-ready, gated | Behind `CHAMADE_FILES_ENABLE_TEAMS`. Flips on post-AppSource cert | | Google Meet | code-ready, gated | code-ready, gated | Behind `CHAMADE_FILES_ENABLE_MEET`. Flips on post-OAuth verification | | SIP | — | — | Audio-only protocol | | Zoom | — | — | SDK-only file transfer, no public API | **Cap:** 25 MB per attachment (configurable via `CHAMADE_FILES_MAX_SIZE_MB`). **Retention:** inbound metadata kept 30 days, agent uploads expire after 24 h. Bytes hot-cached 15 min then re-fetched on demand. Zero long-term storage — if the platform evicts, Chamade returns 410 Gone. ### Upload a File — `POST /api/files` Three input paths: - **Multipart** (`Content-Type: multipart/form-data`) — field `file`, optional `name`, `mime`. Best path for local files: curl streams the bytes, the tool/HTTP call stays tiny, no base64 overhead. - **JSON with `url`** — Chamade fetches server-side. SSRF-guarded: every A/AAAA record resolved, private IPs + AWS/GCP metadata endpoints (`169.254.169.254`, `fd00:ec2::254`) blocked, redirects re-validated. - **JSON with `bytes_b64`** — inline base64. Requires `name` + `mime`. Use only for tiny files; 100 KB of base64 can stall an LLM 30–60 s. ```bash # A — multipart (best for local files) curl -X POST https://chamade.io/api/files \ -H "X-API-Key: chmd_..." \ -F "file=@./report.pdf" # B — JSON url curl -X POST https://chamade.io/api/files \ -H "X-API-Key: chmd_..." \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/report.pdf", "name": "report.pdf"}' # C — JSON bytes_b64 (tiny files only) curl -X POST https://chamade.io/api/files \ -H "X-API-Key: chmd_..." \ -H "Content-Type: application/json" \ -d '{"bytes_b64": "iVBORw0K...", "name": "pixel.png", "mime": "image/png"}' ``` Response: ```json { "file_id": "f_abc...", "url": "https://chamade.io/api/files/f_abc...", "name": "report.pdf", "mime": "application/pdf", "size": 123456, "expires_at": "2026-04-18T12:00:00Z" } ``` Pass the `file_id` in subsequent `attachments: [{file_id}]`. Uploads are user-private — a stolen `file_id` cannot be used from another account. ### Pre-signed upload URL — `POST /api/files/reserve` Reserve a `file_id` and get back a short-lived (5 min) pre-signed URL that accepts a multipart POST with **no auth header** — the URL itself is the auth. Single-use. This is the path the MCP tool `chamade_file_upload_url` exposes; useful when the uploader (shell script, CI job) should not see the API key. ```bash # 1. Reserve curl -X POST https://chamade.io/api/files/reserve \ -H "X-API-Key: chmd_..." \ -H "Content-Type: application/json" \ -d '{"name": "report.pdf", "mime": "application/pdf"}' # → {"file_id": "f_...", "upload_url": "https://chamade.io/api/files/u/...", ...} # 2. Upload (no auth header) curl -F "file=@./report.pdf" https://chamade.io/api/files/u/... # 3. Send curl -X POST https://chamade.io/api/dm/chat \ -H "X-API-Key: chmd_..." \ -H "Content-Type: application/json" \ -d '{"platform": "nctalk", "attachments": [{"file_id": "f_..."}]}' ``` ### Send with attachments `POST /api/dm/chat` and `POST /api/call/{id}/chat` both accept an `attachments` array. Three per-entry shapes: | Shape | When to use | |-------|-------------| | `{"file_id": "f_..."}` | Uploaded via `POST /api/files` or the pre-signed URL. Reusable across conversations. Recommended for any non-trivial file. | | `{"url": "https://...", "name": "...", "mime": "..."}` | Re-send an inbound attachment, or a public web resource. SSRF-guarded server-side fetch. | | `{"bytes_b64": "...", "name": "...", "mime": "..."}` | Inline tiny files only. Full payload round-trips through JSON. | When `attachments` is set, `text` is optional and becomes the caption of the first attachment. On DMs, sync-reply fast-path is skipped if attachments are present — the send goes async (fine, just one extra Maquisard hop). ### Serve a File — `GET /api/files/{token}` / `HEAD /api/files/{token}` Stream the bytes. The `token` path param **is** the auth (24 bytes url-safe, ~128 bits entropy). No `X-API-Key` header required — hand the URL to external tools (vision models, OCR, transcription) without leaking your key. On cache miss, Chamade re-fetches via a platform-specific refetcher (Telegram `getFile`, Slack bearer, Meta Graph `/{media_id}`, NC Talk WebDAV, Discord CDN). Concurrent lock per token dedupes re-fetches. Platform-evicted files return 410 Gone. ### Attachments in the Inbox `GET /api/inbox/{conversation_id}` enriches each message with `attachments[]`: ```json { "messages": [{ "id": "msg_...", "text": "Here's the report", "direction": "inbound", "attachments": [{ "name": "report.pdf", "mime": "application/pdf", "size": 123456, "url": "https://chamade.io/api/files/f_abc..." }] }] } ``` Same shape on push / long-poll events (`dm_chat`, `call_chat`) and on the MCP `chamade_inbox` tool. --- ## Account Management API All account management endpoints use the same authentication as the rest of the API: `X-API-Key` header or session cookie. The only action that requires a human is creating the initial account (signup). ### API Keys ``` GET /api/api-keys ``` List all API keys for the authenticated user. Returns key prefix, name, created/last-used dates. ``` POST /api/api-keys ``` Create a new API key. Requires verified email. Returns the full key (shown only once). **Request body:** ```json {"name": "My bot"} ``` ``` DELETE /api/api-keys/{key_id} ``` Revoke an API key. ### Bot Tokens (BYOB) Register your own Discord, Telegram, or Slack bot so it appears under your brand. ``` GET /api/bot-tokens ``` List registered bot tokens. ``` POST /api/bot-tokens ``` Register a new bot token. Requires verified email. **Request body:** ```json { "platform": "discord", "bot_token": "MTIz...", "discord_app_id": "123456789" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `platform` | string | Yes | `discord`, `telegram`, or `slack` | | `bot_token` | string | Yes | Bot token from the platform | | `discord_app_id` | string | Discord only | Discord application ID | | `slack_signing_secret` | string | Slack only | Signing secret for event verification | ``` DELETE /api/bot-tokens/{bot_id} ``` Delete a bot token (unregisters from bridge). ### Platform Connections ``` GET /api/platforms ``` List OAuth connections and available providers. Shows connection status for Microsoft, Google, Discord, Telegram, Slack. ``` POST /api/oauth/start/{provider} ``` Start an OAuth flow. Returns `{"auth_url": "https://..."}` — open this URL in a browser to authorize. Provider: `microsoft`, `google`, or `discord`. ``` DELETE /api/connections/{connection_id} ``` Disconnect a platform (revoke OAuth connection). ### Telegram Connect ``` POST /api/telegram/connect ``` Generate a Telegram deep link for connecting a Telegram account. Returns `{"deeplink": "https://t.me/..."}`. ### Slack Install ``` POST /api/slack/install ``` Generate a Slack OAuth URL for installing the bot in a workspace. Returns `{"url": "https://slack.com/oauth/..."}`. ### Invite Links ``` POST /api/invite-link ``` Generate a temporary invite token (15 min, single-use) for WhatsApp/Telegram messaging. ### Profile ``` PATCH /api/profile ``` Update user profile. **Request body:** ```json {"name": "New Name"} ``` ``` POST /api/change-password ``` Change password (requires current password). **Request body:** ```json {"current_password": "old", "new_password": "new"} ``` ``` POST /api/change-email ``` Request email change (sends verification to new address). ``` DELETE /api/account ``` Delete account and all data (GDPR right to erasure). Requires password confirmation. ``` GET /api/export ``` Export all user data as JSON (GDPR right to data portability). ### Usage ``` GET /api/usage ``` Get usage summary: call count, concurrent call limits, platform activity. In early access, the returned fields are informational only — there is no billing cutoff. ### SIP Management (BYOT only) ``` POST /api/sip/trunk ``` Connect a SIP trunk (BYOT). **Request body:** ```json { "sip_host": "sip.example.com", "sip_port": 5060, "sip_username": "user", "sip_password": "pass", "sip_realm": "sip.example.com", "sip_caller_id": "+33612345678" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `sip_host` | string | Yes | SIP server hostname | | `sip_port` | int | No | SIP server port (default 5060) | | `sip_username` | string | Yes | SIP username | | `sip_password` | string | Yes | SIP password | | `sip_realm` | string | Usually | SIP authentication realm — required by most commercial providers (OVH: `sip-domain.io`). REGISTER will fail with 404 if your provider expects a realm. | | `sip_caller_id` | string | No | Caller ID for outbound calls | ``` GET /api/sip/trunk ``` Get SIP trunk info (host, port, username, realm, caller_id — not password). ``` DELETE /api/sip/trunk ``` Disconnect SIP trunk and release all BYOT DIDs. ``` POST /api/sip/trunk/dids ``` Add a BYOT DID (phone number in E.164 format). ``` DELETE /api/sip/trunk/dids/{did_id} ``` Remove a BYOT DID. ``` POST /api/sip/trunk/dids/{did_id}/settings ``` Update settings for a BYOT DID (auto_answer toggle). **Request body:** ```json {"auto_answer": true} ``` ### Billing > **Early access: Chamade is free and open to everyone. There is no billing flow in use.** > The `/api/billing/*` endpoints exist in code (Stripe is wired) but are not used or documented in early access. Pricing will be announced once the feature surface stabilizes. If you are planning production-scale usage and want to discuss SLAs ahead of the pricing reveal, drop a note to contact@nafis.io. --- ## MCP Server Chamade runs a hosted MCP server at `https://mcp.dev.chamade.io/mcp/` speaking the Streamable HTTP transport from the 2025-03-26 MCP spec. It exposes 15 tools (calls, messaging, file upload) and 1 resource template (`chamade://calls/{call_id}/transcript`). Two auth paths are accepted in parallel: - **Static bearer** (`Authorization: Bearer chmd_*`) — same API key as the REST API. Best for config-file clients (Claude Desktop, Claude Code, Cursor, Windsurf, stdio shim). - **OAuth 2.1** (MCP authorization spec — RFC 9728 PRM, RFC 7591 DCR, RFC 8414 / OIDC discovery, PKCE S256) — for hosts that add MCP servers via an in-browser sign-in. Used by **claude.ai Custom Connectors**: paste the URL, approve once on the consent screen, done — no API key to manage. There's nothing to install: the server is hosted on Chamade's infrastructure. Your MCP client just needs an HTTP connection and the API key. The default URL runs stateless (per-request transport, restart-proof, polling-only); clients that want real-time push append `?stateful` to the URL and — for Claude Code — launch with a per-session channel flag. See [Channel Mode](#channel-mode-real-time-push) below for details. Other MCP clients silently fall back to polling and work unchanged. ### Setup — HTTP direct (recommended) For MCP clients that support Streamable HTTP — Claude Desktop (recent), Claude Code, Cursor, Windsurf, and most 2026+ clients: ```json { "mcpServers": { "chamade": { "type": "http", "url": "https://mcp.dev.chamade.io/mcp/", "headers": { "Authorization": "Bearer chmd_your_key_here" } } } } ``` Drop that in the client's MCP config file (`.mcp.json`, `claude_desktop_config.json`, `.cursor/mcp.json`, etc.) and restart the client. That's it. ### Setup — stdio shim for legacy clients For clients that only speak stdio, use the `@chamade/mcp-server@3` npm package. Since v3 it's a thin wrapper around `mcp-remote` that bridges stdio to the same hosted HTTP endpoint — every tool, every resource, every push event, no drift. ```json { "mcpServers": { "chamade": { "command": "npx", "args": ["-y", "@chamade/mcp-server@3"], "env": { "CHAMADE_API_KEY": "chmd_your_key_here", "CHAMADE_URL": "https://dev.chamade.io" } } } } ``` Requires Node.js 18+. First run downloads `mcp-remote` as a local dependency; subsequent launches are instant. ### Setup — OAuth sign-in (claude.ai Custom Connectors) For clients that add MCP servers via an in-browser sign-in flow instead of a config file. Paste the server URL alone — no API key needed: ``` https://mcp.dev.chamade.io/mcp/ ``` On add, the client runs the MCP OAuth 2.1 flow: discovery (`/.well-known/oauth-protected-resource/mcp` → `/.well-known/oauth-authorization-server/mcp`), Dynamic Client Registration, redirect to the Chamade consent screen at `https://dev.chamade.io/mcp-auth/consent`, access-token exchange (PKCE S256), then the regular tool calls. Tokens rotate on refresh (~1 h access / ~30 d refresh). Consent grants can be revoked from the dashboard. ### Tools | Tool | Description | |------|-------------| | `chamade_call_join` | Join a voice meeting. Returns a call_id, capabilities, and — for voice calls — an `audio` block with the raw-PCM WebSocket endpoint you can wire to your own stack as an alternative to hosted STT/TTS. With a provider preset in /dashboard → Voice providers, transcripts start flowing automatically and you can speak with `chamade_call_say`. | | `chamade_call_chat` | Send a text chat message in the meeting. Works on all platforms, free, no audio involved. | | `chamade_call_status` | Get call status and new transcript lines (delta pattern). Transcripts flow here when you have an STT preset enabled in /dashboard → Voice providers. Without a preset, your own STT client (reading from the call WebSocket) is the source of truth instead. | | `chamade_call_accept` | Answer a ringing inbound call (SIP, Teams DM call, etc.). | | `chamade_call_refuse` | Refuse/reject a ringing inbound call. | | `chamade_call_typing` | Send a typing indicator in meeting chat. | | `chamade_call_leave` | Hang up and leave the meeting. | | `chamade_call_list` | List all active calls. | | `chamade_call_say` | **[BYOK]** Speak text via hosted TTS. Requires a TTS preset in /dashboard → Voice providers (BYOK — you provide the ElevenLabs / Deepgram / … key, Chamade runs the pipeline for free). Returns 400 when no preset is configured — prefer BYO TTS via the call WebSocket in that case. | | `chamade_inbox` | Check DM conversations (Discord, Telegram, Teams, WhatsApp, Slack, NC Talk). Three modes: snapshot, per-platform detail, delta with optional long-poll up to 55 s. Shows the WhatsApp 24 h window state inline. | | `chamade_dm_chat` | Send a DM message by platform. On WhatsApp outside the 24 h window, returns HTTP 202 `{status: "queued"}` and auto-fires a re-engagement template. | | `chamade_dm_typing` | Send a typing indicator in DM by platform. | | `chamade_file_upload_url` | Mint a short-lived (5 min) pre-signed URL for uploading a local file. Your agent runs `curl -F file=@path ` (no auth header — the URL is the auth), then passes the returned `file_id` in `attachments: [{file_id}]` on `chamade_dm_chat` or `chamade_call_chat`. This is the correct pattern for any non-trivial local file: the shell streams the binary and the tool call stays ~80 chars, versus `bytes_b64` inline which forces the model to emit every base64 character through its own output budget (30–60s stall for a 100 KB image). | | `chamade_account` | Check account status — plan, `features` block (which features are `ready` vs `byok`), per-platform readiness + capabilities, and the identity map. Call this first on cold start to bootstrap. | ### Resource | URI template | Description | |---|---| | `chamade://calls/{call_id}/transcript` | **[BYOK]** Live transcript of an active call from Chamade's hosted STT. Only populated when you have an STT preset enabled in /dashboard → Voice providers (BYOK — Chamade runs the pipeline with your key for free). In BYO audio mode (no preset), your own STT client is the source of truth — this resource will return empty. | ### Environment variables (stdio shim only) These only apply to the `@chamade/mcp-server@3` shim. The HTTP-direct config doesn't use env vars — everything is in the JSON config. | Variable | Required | Description | |----------|----------|-------------| | `CHAMADE_API_KEY` | Yes | Your API key (`chmd_...`) | | `CHAMADE_URL` | No | Chamade instance base URL. Default: `https://chamade.io`. Set this to `https://dev.chamade.io` if using this instance — the shim derives the MCP endpoint automatically. | | `CHAMADE_MCP_URL` | No | Explicit MCP endpoint override. Skip this unless you're running a custom proxy. | --- ## Channel Mode (real-time push) Channel mode pushes events into the MCP session as they happen — new DMs, call state changes, inbound SIP/Teams calls, WhatsApp `dm_delivered` flushes. No polling. `call_transcript` events are pushed only when hosted STT is enabled; in BYO audio mode your own STT client is the source of truth. ### Setup Two pieces. Server-side is one URL flag (`?stateful`); client-side needs a per-launch command-line flag. **Server side — add `?stateful` to the MCP URL.** The default URL (`https://mcp.dev.chamade.io/mcp/`, no query param) runs in stateless mode: each tool call is a self-contained HTTP request, no server-allocated session ID, no push channel. Transparent to chamade restarts. To enable push you explicitly opt into a persistent session: ```json { "mcpServers": { "chamade": { "type": "http", "url": "https://mcp.dev.chamade.io/mcp/?stateful", "headers": { "Authorization": "Bearer chmd_..." } } } } ``` For the `@chamade/mcp-server@3` stdio shim, pass `--stateful` in the `args`. Both forms append the same `?stateful` query param to the underlying HTTP URL. **Client side — Claude Code launch flag (REQUIRED, on top of `?stateful`).** Claude Code only processes `notifications/claude/channel` messages from MCP servers you've explicitly opted in via a per-launch command-line flag. Without it, the server will push events into the void and tools fall back to polling mode: ```bash # Required on every Claude Code launch claude --dangerously-load-development-channels server:chamade --continue ``` The `server:chamade` target must match the key in your `.mcp.json`. If you have multiple Chamade entries (prod + dev, HTTP + shim, etc.), pass the flag once per entry: `server:chamade server:chamade-dev`. The flag is transport-agnostic — required for HTTP direct, the `@chamade/mcp-server@3` stdio shim, and `mcp-remote` direct, and for any MCP server not on Anthropic's approved channels allowlist. It's a research-preview opt-in; when Chamade gets added to the allowlist the flag will become optional. **Cost of stateful mode.** A chamade redeploy kills the persistent session. Per the MCP spec a client should re-initialize on HTTP 404; Claude Code doesn't always do that today, so you may see a brief "MCP disconnected" blip until you relaunch the client. Only enable stateful mode if you actually need push. For everything else (Claude Desktop, Cursor, Windsurf, any polling-first harness) leave the URL bare and use `chamade_inbox` long-polling — see [Clients without channel support](#clients-without-channel-support) below. ### How it works 1. The client opens an MCP session and sends `initialize` to `https://mcp.chamade.io/mcp/?stateful`. 2. The hybrid session manager detects the `?stateful` param and routes to a persistent session — issuing an `Mcp-Session-Id` response header. Chamade also advertises `experimental.claude/channel: {}` in its capabilities. 3. If the client supports channel mode (Claude Code with the launch flag), it opens a `GET /mcp/` SSE stream using the session ID — the canonical "I want server-initiated messages" signal. That GET is what triggers chamade to start a per-session bridge subscribed to the user's inbox hub. Without SSE, no bridge — so clients that ignore the channel cap pay no server-side cost beyond the session ID itself. 4. When any event is published to the hub (DM webhook, Maquisard bridge, SIP trunk, etc.), the bridge forwards it to the live SSE connection as a `notifications/claude/channel` JSON-RPC notification with the event JSON in the `content` field. 5. Channel-aware clients surface that content to the agent, which reacts with normal tool calls (`chamade_call_say`, `chamade_dm_chat`, …). 6. When the client disconnects (TCP drop, SSE close, transport termination), the bridge cleans up automatically — an idle liveness probe catches stale connections within ~25 s even if no events are flowing, and the hub subscription is released. Do **not** call `chamade_call_status` or `chamade_inbox` in a loop when channel mode is active — events arrive on their own. You can still call those tools manually to catch up after a reconnect. ### Self-test events (`chamade_probe`) When the operator clicks **Ping agents** on `/dashboard/keys`, Chamade pushes a synthetic `dm_chat` event into your channel with `platform="chamade_probe"` and sender `"Chamade Dashboard"`. It's not a real user message — it's the dashboard verifying the Chamade → channel → agent loop works end-to-end, and attributing each reply to the specific API key that authenticated the `chamade_dm_chat` call (so multi-key accounts can tell which agent replied). Handle it like any other `dm_chat`: call `chamade_dm_chat(platform="chamade_probe", text="…")` with any non-empty short text. Don't invent a "Chamade Dashboard" contact, don't add it to memory, don't escalate — a trivial acknowledgement is exactly what the operator wants. The timeout is 30 s; anything longer shows up as `✗ no reply` next to your key. ### Clients without channel support Clients that don't understand the `experimental.claude/channel` capability silently ignore the push notifications. Tools still work normally in polling mode: - **Transcripts:** call `chamade_call_status` in a loop (delta pattern — only new lines each time). - **DMs:** call `chamade_inbox` with a `last_message_cursor` argument. Optionally add `wait=55` for long-polling — server-side the call blocks up to 55 s waiting for a new message, near-real-time latency without a WebSocket. **Starter prompt for polling agents.** Drop this paragraph in your system prompt on non-Claude-Code clients (Cursor, Windsurf, Claude Desktop, custom harnesses) — it spells out the polling loop the client otherwise has to implement manually: > You have access to the Chamade MCP tools. Chamade delivers inbound DMs via polling on this client. On startup, call `chamade_account` once to get the `last_message_cursor` field from the response and remember it. Then, between user turns (or in a dedicated background loop if your harness supports one), call `chamade_inbox` with that cursor AND `wait=55` — the request blocks server-side up to 55 seconds and returns as soon as a new DM arrives (or 55s expires). Save the new `last_message_cursor` from each response for the next call. For every conversation in `.conversations` with new messages, reply via `chamade_dm_chat(platform=…, text=…)` within 60 seconds — Chamade auto-sends a typing indicator while you think, and a fallback "no response received" message if you miss the window. When a `whatsapp_window` block reports `open=false`, your send will return HTTP 202 queued and a re-engagement template is fired automatically; nothing extra to do on your side. Start the poll loop immediately on startup and keep it running indefinitely — only stop if the user explicitly asks you to. Every idle moment should be spent in a `chamade_inbox` long-poll. - **Active calls:** call `chamade_call_list` periodically to detect new ringing entries. ### Channel mode vs polling | | Polling | Channel mode | |---|---|---| | DM messages | Agent polls `chamade_inbox` (optional long-poll up to 55 s) | Pushed automatically | | Incoming calls | Agent polls `chamade_call_list` | Pushed automatically | | Call state changes | Agent polls `chamade_call_status` | Pushed automatically | | Transcripts | N/A in BYO audio — your STT client is the source | Pushed when hosted STT is enabled (BYOK — add a provider key in the dashboard) | | Latency | Depends on poll interval | Real-time (<1 s) | | Clients | Any MCP client | Claude Code only (launched with `--dangerously-load-development-channels server:chamade`) | | Server-side config | Same | Same — no extra flag needed in `.mcp.json` | | Client-side config | Nothing | Claude Code launch flag required every session | --- ## Platform Setup Details ### Discord Three options: 1. **DM only (text):** Connect Discord account via `POST /api/oauth/start/discord` — returns `{"auth_url": "..."}`. The user opens the URL in a browser to authorize. After that, users DM the Chamade bot. Capabilities: read, write, typing. 2. **Shared bot on your server (voice + text):** Same OAuth connect as above. Then invite the Chamade bot to the Discord server (the invite link is returned by `GET /api/platforms`). Capabilities: audio_in, audio_out, read, write, typing. 3. **Custom bot (your branding):** Create a Discord application, enable Message Content Intent, generate an invite URL with bot scope (Connect, Speak, Send Messages, Read Message History). Register the bot via `POST /api/bot-tokens` with `{"platform": "discord", "bot_token": "...", "discord_app_id": "..."}`. Capabilities: audio_in, audio_out, read, write, typing. ### Microsoft Teams 1. Download the Chamade Teams app from chamade.io/download/teams-app 2. In Teams: Apps > Manage your apps > Upload a custom app 3. In Dashboard > Platforms: Connect Microsoft (OAuth) Custom app sideloading must be enabled by your Teams admin. Capabilities: audio_in, audio_out, read, write, typing. ### Google Meet Connect your Google account in Dashboard > Platforms. The agent joins existing meetings and reads/posts in the in-meeting chat. Google Meet supports `audio_in` (Chamade streams raw PCM from the meeting to your agent) and chat (`read`/`write`), but not `audio_out` — the Meet connector cannot currently inject audio back into the meeting. Use `POST /api/call/{id}/chat` (or `chamade_call_chat`) to send text messages instead. > ⚠️ **Beta limitation — voice in mixed meetings.** Real-time audio capture on Meet relies on Google's Meet Media API, which is currently in **Developer Preview**. Every participant in the meeting must be individually enrolled in Google's Developer Preview Program — if anyone isn't, audio capture fails. In practice this means audio capture only works in meetings between enrolled accounts. Calendar integration and in-meeting chat work normally for everyone. **For production voice use cases, recommend Microsoft Teams, Discord, Nextcloud Talk, or SIP.** ### Zoom (beta) Zoom requires a connected account. Setup: 1. Get beta access (contact us to be added as tester on Zoom Marketplace) 2. Install the Chamade app from the Zoom Marketplace 3. Connect your Zoom account in the dashboard (OAuth) Then provide the meeting URL: ```json {"platform": "zoom", "meeting_url": "https://zoom.us/j/1234567890?pwd=abc123"} ``` Supported URL formats: - `https://zoom.us/j/1234567890?pwd=abc123` - `https://us02web.zoom.us/j/1234567890?pwd=abc123` - `https://zoom.us/my/personal-room` Zoom uses 16 kHz PCM audio. Capabilities: audio_in, audio_out, read, write. The agent joins with the user's Zoom identity (ZAK token). Works for meetings in the user's own Zoom account. ### Telegram **Shared bot:** The user must first connect their Telegram account: 1. Call `POST /api/telegram/connect` — returns `{"deeplink": "https://t.me/...?start=TGLINK_..."}` 2. The user opens the deeplink in Telegram and sends the `/start` message to the bot 3. The account is now linked — messages appear in `chamade_inbox` Without step 1-2, the user's Telegram account is NOT connected and messages will not be routed to this Chamade account. The `chamade_account` status will show `"available (shared bot, user account not connected)"` until the deeplink is used. **Custom bot:** Create a bot via @BotFather (`/newbot`), copy the token, register it via `POST /api/bot-tokens` with `{"platform": "telegram", "bot_token": "..."}`. Capabilities: read, write, typing. ### WhatsApp Text only, no setup needed on the user's side. Generate a temporary invite link (valid 15 minutes, single-use) from the Dashboard. The conversation appears in the agent's inbox. WhatsApp enforces a 24-hour customer service window: after a user sends a message, the agent can reply freely with plain text for 24 hours. **Outside that window**, Chamade automatically switches to re-engagement templates — you don't have to handle this yourself. Your `POST /api/dm/chat` call returns `202 Accepted` with `{status: "queued"}` instead of `200 OK`, and Chamade fires a pre-approved `agent_followup` template to nudge the user. When they reply, your queued messages flush automatically in chronological order and you receive a `dm_delivered` WebSocket event listing the delivered message IDs. Pending messages that stay unread for more than 7 days are dropped. Capabilities: read, write. ### Slack **Shared bot:** Install the Chamade Slack app via Dashboard. DM the bot or invite to a channel. **Custom bot (BYOB):** Create a Slack app at api.slack.com/apps. Required scopes: `chat:write`, `im:history`, `im:read`, `channels:history`, `channels:read`, `groups:history`, `groups:read`, `users:read`, `files:read`, `files:write`. Set event subscriptions to `https://chamade.io/api/slack/webhook`. Subscribe to: `message.im`, `message.channels`, `message.groups`, `app_mention`. Slack does not support typing indicators for bots. Capabilities: read, write. ### Nextcloud Talk Requires installing the Chamade Talk addon on your Nextcloud instance (admin + SSH access required). Then connect via Dashboard > Platforms. The bot is scoped to your Nextcloud account: - **DMs:** Only you can DM the bot - **Group rooms:** Type `/activate` to enable the bot in a room, `/deactivate` to disable ```json {"platform": "nctalk", "meeting_url": "https://cloud.example.com/call/abc123"} ``` Capabilities: audio_in, audio_out, read, write, typing. ### SIP / Phone SIP is **BYOT only** — Chamade does not provide phone numbers. You bring your own SIP trunk + your own DIDs. **Setup (one-time):** 1. Connect your SIP trunk in Dashboard → Platforms → SIP. Chamade keeps a REGISTER session open with your provider so inbound calls reach the agent. Realm is usually required (e.g. `sip-domain.io` for OVH). 2. Add each phone number (DID) you want to receive calls on, in E.164 format (`+33612345678`). **Without at least one DID added, no inbound call will reach your agent** — Chamade rejects unknown DIDs. 3. Per-DID toggle: `auto_answer` skips the `ringing` state and goes straight to `active`. **Inbound:** Caller dials your DID → Chamade emits `call_invite` → if `auto_answer=false`, agent answers via `POST /api/call/{id}/accept` (60s timeout → `missed`). If `true`, the call is `active` immediately. **Outbound:** ```json {"platform": "sip", "meeting_url": "sip:+33612345678@sip.example.com"} ``` Requires a connected trunk (uses its credentials per-call). Caller ID is the trunk's `sip_caller_id` if set. Capabilities: audio_in, audio_out. --- ## Plans & Pricing **Chamade is currently in early access — free and open to everyone, no plans, no billing, no quota.** All features are available without restriction. Concurrent call limits are generous and informational only. Pricing will be announced once the feature surface stabilizes. For production-scale usage or SLA discussions, reach out at contact@nafis.io — optional, not gated. --- ## Error Codes ### HTTP Errors | Error | Cause | Fix | |-------|-------|-----| | `401` Invalid or missing API key | Missing or incorrect `X-API-Key` header | Check your API key in the Dashboard | | `400` Hosted TTS not configured | No TTS preset enabled on the call | Add a TTS provider (ElevenLabs / Deepgram / …) in /dashboard → Voice providers, or use BYO audio on the call WebSocket | | `404` Call not found | Call ID does not exist or has ended | Use `GET /api/calls` to list active calls | | `429` Concurrent call limit reached | Rate limit hit | End an active call first or retry shortly | ### Platform Errors | Error | Cause | Fix | |-------|-------|-----| | `400` No discord bot registered | No Discord bot token in dashboard | Add a Discord bot token in Dashboard > My Bots, or connect the shared bot | | `400` No telegram bot registered | No Telegram bot token in dashboard | Add a Telegram bot token in Dashboard > My Bots | | `400` meeting_url required | No meeting URL provided and no OAuth connected | Provide a meeting URL, or connect your account via OAuth | | `400` No SIP trunk configured | Trying outbound SIP without a trunk | Connect a SIP trunk in Dashboard > SIP | | `409` No active WebSocket connection | Bridge connection not ready yet | Wait a moment for the connection to establish | ### Recovery from Disconnection If the bridge restarts or the connection is lost, the call state becomes `"disconnected"`. To recover: call `POST /api/call` again with the same platform and meeting URL. A new call ID is issued. The previous transcript is not carried over. If your agent polls `chamade_call_status` regularly, it will detect disconnections automatically and can reconnect without user intervention.