# Chamade — Complete Documentation > Voice and chat gateway for AI agents. Chamade joins meetings, phone calls, and DMs on 10+ platforms and exposes the audio as a uniform PCM WebSocket. You bring your own STT/TTS stack and plug it in. Chamade is a **voice and chat gateway** in the infrastructure sense: a single uniform API to join meetings, phone calls, and DMs across Discord, Teams, Meet, SIP, Telegram, WhatsApp, Slack, NC Talk, and Zoom. Your agent joins a call, receives raw PCM audio on a WebSocket, and streams PCM back. You bring your own speech-to-text, language model, and text-to-speech — Chamade handles the transport only. For text conversations (DMs, group chats, in-call chat, inbox), Chamade is fully plug-and-play: your agent uses the MCP tools (`chamade_inbox`, `chamade_dm_chat`, `chamade_call_chat`) and never touches raw audio — it's all JSON text messages. Hosted STT/TTS (where Chamade runs the speech layer for you) is **in development and beta-gated** for early access. Contact contact@nafis.io to request supervised access if you want to try it. For production voice, use BYO audio. Works with any backend that speaks HTTP + WebSocket: AI agents, voice bots, scripts, orchestration frameworks (LiveKit Agents, Pipecat, Vocode, custom stacks). Chamade is fully bot-friendly. 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. --- ## Quick Start ### 1. Get an API Key Sign up at https://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.chamade.io/mcp/", "headers": { "Authorization": "Bearer chmd_..." } } } } ``` For Claude Code specifically, add the channel-mode launch flag every time you start Claude Code to receive real-time push events (new DMs, ringing calls, call state changes). Without it, the tools still work in polling mode. Full details and the exact 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_..." } } } } ``` **REST API** — direct HTTP calls: ```bash curl -X POST https://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 | Activate a phone number or bring your own trunk | --- ## Supported Platforms | Platform | Voice | Chat | Setup | |----------|-------|------|-------| | Discord | audio_in, audio_out | read, write, typing | Invite Chamade bot or bring your own | | Microsoft Teams | audio_in, audio_out | read, write, typing | Install Teams app + connect Microsoft account | | Google Meet | audio_in | read, write | 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 | DM the Chamade bot or bring your own | | WhatsApp | — | read, write | Share a link — no setup needed | | Slack | — | read, write | Install Chamade app or bring your own | | Nextcloud Talk | audio_in, audio_out | read, write, typing | Connect via dashboard | | SIP / Phone | audio_in, audio_out | — | Answering machine or bring your own trunk | **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** — file attachments supported 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) — [BETA, gated] ``` POST /api/call/{call_id}/say ``` > **Beta-gated: hosted TTS is in development** and likely to fail on accounts that aren't on the supervised beta program. Contact contact@nafis.io to request access. For production, use **BYO audio** instead — connect your own TTS (OpenAI Realtime, ElevenLabs, Cartesia, Deepgram, etc.) to the call's audio WebSocket and stream PCM directly. See the "Audio Streaming (BYO)" section below. Speak text aloud in the meeting via Chamade's hosted text-to-speech engine. 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 `beta_gated`) - `features_note` — plain-English explanation of what's gated and how to request access - `platforms` — per-platform `{status, capabilities}` 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`) - `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. The returned `features` block is the quickest way to see which features are ready vs gated for early-access users. --- ## 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 (recommended, default) This is the **primary way** to do voice with Chamade. The same call WebSocket exposes the room's raw PCM audio, bidirectionally: you receive what the humans say (as binary frames), and you can push what your agent says back (also as binary frames). Chamade is a transport layer — it does not run STT or TTS for you in this mode. You 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) > `transcripts: false` is the default on `POST /api/call`. In BYO mode you can leave it as-is — Chamade runs zero server-side STT. If you want hosted STT in addition to BYO audio (contact us for beta access), pass `transcripts: true` explicitly. 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. --- ## 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 ``` GET /api/sip/number ``` Get the user's assigned pool SIP number (answering machine). ``` POST /api/sip/activate ``` Activate answering machine — claim a DID from the pool. Optionally pass `{"country": "FR"}` (ISO 3166-1 alpha-2) to pick a specific country. **Response:** ```json {"status": "ok", "phone_number": "+33...", "label": "France"} ``` ``` GET /api/sip/countries ``` List countries with available pool DIDs. ``` DELETE /api/sip/deactivate ``` Deactivate answering machine — release the pool DID. ``` POST /api/sip/settings ``` Update SIP settings (auto_answer toggle). ``` 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 | Yes | SIP server port | | `sip_username` | string | Yes | SIP username | | `sip_password` | string | Yes | SIP password | | `sip_realm` | string | No | SIP authentication 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. Contact contact@nafis.io if you need to discuss production-scale commercial usage. --- ## MCP Server Chamade runs a hosted MCP server at `https://mcp.chamade.io/mcp/` speaking the Streamable HTTP transport from the 2025-03-26 MCP spec. It exposes 13 tools (calls + messaging) and 1 resource template (`chamade://calls/{call_id}/transcript`). Auth is a standard `Authorization: Bearer chmd_*` header — the same API key you use for the REST API. 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 server always declares the `experimental.claude/channel` capability during the initialize handshake; Claude Code still needs a per-launch opt-in flag to process those push notifications — see [Channel Mode](#channel-mode-real-time-push) below. 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.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" } } } } ``` Requires Node.js 18+. First run downloads `mcp-remote` as a local dependency; subsequent launches are instant. ### Tools | Tool | Description | |------|-------------| | `chamade_call_join` | Join a voice meeting. Returns a call_id, capabilities, and an `audio` block describing the PCM WebSocket endpoint to connect your own STT/TTS stack to. | | `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 are empty in BYO audio mode — they come from your STT client 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` | **[BETA — gated]** Speak text via hosted TTS. Gated for early access; prefer BYO TTS via the call WebSocket. Contact contact@nafis.io for beta. | | `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_account` | Check account status — plan, `features` block (which features are ready vs beta_gated), 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` | **[BETA — gated]** Live transcript of an active call from Chamade's hosted STT. Only populated when hosted STT is enabled for your account. In BYO audio mode (default), 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://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. > **What about transcripts?** Channel mode can push `call_transcript` events, but only when hosted STT is enabled for your account (beta-gated). In the default BYO audio mode, transcripts flow through your own STT client over the call's audio WebSocket and do not go through the MCP channel. Agents operating in BYO mode should treat `call_transcript` as a non-event and keep reading their own STT output. ### Setup Two pieces. Server-side is automatic; client-side needs a per-launch flag. **Server side — nothing to configure.** The hosted Chamade MCP server at `https://mcp.chamade.io/mcp/` always declares the `experimental.claude/channel` capability during the initialize handshake. The `.mcp.json` snippets above are the complete server-side config. **Client side — Claude Code launch flag (REQUIRED).** 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 capability declaration is seen but ignored 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. **Other MCP clients** (Claude Desktop, Cursor, Windsurf) currently do not have an `experimental.claude/channel` receiver and will silently drop push notifications. They still work in polling mode via `chamade_inbox` with `last_message_cursor` + optional `wait=55` long-polling, and `chamade_call_status` for transcript deltas. ### How it works 1. The client opens an MCP session and sends `initialize`. 2. Chamade responds with capabilities, including `experimental.claude/channel: {}`. 3. Client decides (based on the per-launch flag for Claude Code) whether to process channel notifications or drop them. 4. On the first discovery request (`list_tools`, `list_resources`, `list_prompts`, `list_resource_templates`), the server eagerly subscribes to the user's inbox hub in the background — no tool call needed. 5. When any event is published to the hub (DM webhook, Maquisard bridge, SIP trunk, etc.), it is forwarded to the live MCP session as a `notifications/claude/channel` JSON-RPC notification with the event JSON in the `content` field. 6. Channel-aware clients surface that content to the agent, which reacts with normal tool calls (`chamade_call_say`, `chamade_dm_chat`, …). 7. When the client disconnects, the background subscription cleans up automatically (an idle liveness probe catches stale sessions within ~30 s even if no events are flowing). 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. ### 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. - **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 only if hosted STT is enabled (beta-gated) | | 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 **Answering machine (inbound):** Activate a routing number in Dashboard. Configure call forwarding. Calls appear with state `"ringing"`. Answer with `POST /api/call/{id}/accept`. Auto-answer available. Unanswered calls time out after 60s. **Bring Your Own Trunk (BYOT):** Connect your SIP trunk in Dashboard. Add DIDs (phone numbers) in E.164 format. Each DID has its own auto-answer toggle. **Outbound SIP (BYOT only):** ```json {"platform": "sip", "meeting_url": "sip:+33612345678@sip.example.com"} ``` 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 except hosted STT/TTS (beta-gated, contact us). Concurrent call limits are generous and informational only. Pricing will be announced once the feature surface stabilizes. Contact contact@nafis.io for production-scale or commercial discussions. --- ## 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 | | `403` Hosted STT/TTS not available | Hosted feature is beta-gated | Use BYO audio via the call WebSocket, or contact contact@nafis.io for beta access | | `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.