API Reference
All endpoints for the Chamade API. Authenticate with your API key via the X-API-Key header.
Authentication
All API calls require the X-API-Key header with your API key:
API keys are created in your Dashboard. They start with chmd_ and are shown only once at creation. Store them securely.
Create Call
POST/api/call
Join a meeting or initiate a call. Returns the call ID and initial state.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | One of: discord, teams, meet, zoom, telegram, slack, 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" |
stt_voice_config_id | string | No | UUID of a specific STT preset to use for this call. Omit to use the user's enabled STT preset (from dashboard → Voice providers). Hosted STT is BYOK — you bring the ElevenLabs / Deepgram key, Chamade runs the pipeline with it for free. No preset configured = BYO-audio (your own STT client reads raw PCM from the call WebSocket). |
tts_voice_config_id | string | No | UUID of a specific TTS preset to use for this call. Omit to use the user's enabled TTS preset. Same BYOK model as STT. No preset configured = POST /api/call/{id}/say returns 400 and you should synthesize locally + push PCM on the audio WebSocket. |
Response:
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:
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
Requires a TTS preset in dashboard → Voice providers (setup). Returns 400 without one — fall back to BYO audio: synthesize locally and push PCM on the call's audio WebSocket.
Speak text aloud in the meeting via Chamade's hosted TTS. Only meaningful on platforms with the audio_out capability.
Request body:
Interrupt TTS (barge-in) — [BYOK]
POST/api/call/{call_id}/stop-speaking
Cancel any in-flight hosted-TTS utterance. No-op if nothing is currently being spoken. No body. Useful when the user starts talking over the agent, or when the agent decides to abandon mid-sentence. MCP equivalent: chamade_call_stop_speaking.
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:
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Depends | Message text (1–10,000 characters). Optional when attachments is set — text then becomes the caption of the first attachment. |
sender_name | string | No | Display name for the sender in meeting chat cards. Default: empty. |
attachments | array | No | File attachments to send with the message. See Files & attachments. Three per-entry shapes: {file_id}, {url, name?, mime?}, or {bytes_b64, name, mime}. Cap 25 MB each. |
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
POST/api/call/{call_id}/typing
Send a typing indicator in the meeting chat. Supported on platforms with the typing capability.
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. This is an alternative to polling GET /api/call/{id} for real-time applications.
Incoming messages (server → client):
Outgoing messages (client → server):
Direct Audio Streaming (BYO audio)
Alternative to hosted STT/TTS: the same WebSocket supports sending and receiving raw PCM, so you can plug in your own voice models — ElevenLabs streaming, OpenAI Realtime, Cartesia Sonic, Deepgram, LiveKit Agents, Pipecat, a manual Whisper cascade, whatever. Recommended when you already have a voice pipeline you want to keep; otherwise the hosted path (Voice providers) is simpler. Chamade is pure transport in this mode — omit stt_voice_config_id / tts_voice_config_id in POST /api/call (or leave no enabled preset) to bypass the speech loop entirely.
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.
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.
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)
If you want 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 text WebSocket continues to deliver
call_transcript,call_chat, andcall_stateevents as JSON text frames in parallel. So you can mix Chamade's STT and 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:
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.
The @chamade/mcp-server npm package exposes text-only tools (chamade_call_say, chamade_call_chat, etc.) and a transcript resource. Direct audio I/O is not exposed via MCP — the stdio JSON-RPC transport doesn't reasonably support raw binary streaming. To stream your own PCM, your agent must connect to this WebSocket directly (alongside or instead of the MCP server).
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) |
limit | int | No | Max conversations to return (default 50) |
status | string | No | Filter by status (default “active”) |
offset | int | No | Pagination offset (default 0) |
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 (by platform)
POST/api/dm/chat
Send a message to the active DM conversation on a platform. This is the recommended endpoint for agents.
Request body:
Same attachments shape as call chat: array of {file_id}, {url, name?, mime?}, or {bytes_b64, name, mime}. See Files & attachments for the per-platform matrix and upload flow. When attachments is set, text becomes the caption of the first attachment and is optional.
Typing Indicator (by platform)
POST/api/dm/typing
Send a typing indicator on a DM conversation.
Request body:
When you call POST /api/dm/typing, the typing indicator stays active automatically (Chamade repeats it internally). It stops when you send a message via POST /api/dm/chat, or after a 65-second safety timeout.
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 your task takes longer than a few seconds:
- Send a short acknowledgment immediately via
POST /api/dm/chatwith"keep_typing": true(e.g. “Looking into it...”). Thekeep_typingflag re-activates the typing indicator automatically after sending. - Do your work, then send the full answer via
POST /api/dm/chat(withoutkeep_typing) - If your work takes more than 60 seconds, call
POST /api/dm/typingevery ~55 seconds to keep the indicator alive
Account Status
GET/api/account
Bootstrap call. Returns plan, a features block (global gateway availability — audio_in, audio_out, text_chat, typing_indicators, files are ready in early access; 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 for free), platforms as a dict of {status, capabilities, files} per platform (the files bool on each platform tells you whether send+receive works end-to-end today — true for Discord, Telegram, Slack, WhatsApp, NC Talk; false for Teams, Meet, SIP, Zoom), concurrent_calls, the identities map, and a last_message_cursor to bootstrap chamade_inbox delta mode.
Files & Attachments
Chamade relays file attachments in both directions on DMs and meeting chat. Every attachment served to your agent comes back as a signed Chamade URL (https://chamade.io/api/files/{token}) — 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.
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 are hot-cached 15 min then re-fetched from the platform on demand. Zero long-term storage: if the platform evicts the file, Chamade responds 410 Gone — same behaviour as the source.
Per-platform matrix
| Platform | Send | Receive | Notes |
|---|---|---|---|
| Discord | ✓ | ✓ | 25 MB (native limit for non-Nitro). CDN URLs signed ~24 h. |
| Telegram | ✓ | ✓ | file_id persists indefinitely; refetch via getFile uses your BYOB bot token or the shared bot. |
| Slack | ✓ | ✓ | Uses files.getUploadURLExternal (the modern path; legacy files.upload was retired 2025-11-12). |
| ✓ | ✓ | Images/audio/video/voice notes/documents/stickers. Voice notes delivered as raw audio/ogg — auto-STT is V2. Meta caption rules apply (no caption on audio). | |
| NC Talk | ✓ | ✓ (addon ≥ 2.4.0) | WebDAV upload + OCS share (shareType=10) via the Chamade bot user. Inbound requires the chamade_talk addon at v2.4.0+ so messageParameters.file is forwarded. |
| Microsoft Teams | code-ready, gated | Wiring complete behind CHAMADE_FILES_ENABLE_TEAMS=false. Flips on once AppSource certification clears — no user-side change needed. | |
| Google Meet | code-ready, gated | Gated behind CHAMADE_FILES_ENABLE_MEET=false while Google OAuth verification is pending. | |
| SIP / Zoom | — | No file channel (audio-only / SDK-only by platform design). | |
Sending attachments
Three per-entry shapes on the attachments array of POST /api/call/{id}/chat and POST /api/dm/chat:
| Shape | When to use |
|---|---|
{"file_id": "f_…"} | Recommended for any non-trivial local file. Upload first via POST /api/files (see below), then reference the returned file_id — including multiple times if you want to re-send the same file across conversations. |
{"url": "https://…", "name": "…", "mime": "…"} | Re-send an inbound attachment, or reference something already hosted on the public web. Chamade fetches the URL server-side (SSRF-guarded — private IPs, loopback, metadata services blocked). |
{"bytes_b64": "…", "name": "…", "mime": "…"} | Tiny inline files only. The full payload round-trips through JSON, so this forces the model (or your HTTP client) to emit every base64 character — a 100 KB image stalls a model for 30–60 s. Prefer file_id via the upload endpoint. |
When attachments is set, text is optional and becomes the caption of the first attachment. The sync-reply fast-path (for DMs) is skipped when attachments are present — the send goes async, which is harmless but slightly slower than a text-only reply.
Upload a File
POST/api/files
Stage a file on Chamade and get a file_id you can reuse across sends.
Three input paths:
- Multipart (
Content-Type: multipart/form-data) — field namefile, optional form fieldsnameandmime. Chamade streams the upload with a size-cap check, so a 25 MB+1-byte payload aborts without committing memory. - JSON with
url— Chamade fetches the URL server-side. SSRF-guarded (private IPs including AWS/GCP metadata are blocked, every A/AAAA record is resolved, redirect chains revalidated). - JSON with
bytes_b64— inline base64 bytes. Requiresname+mime. Use only for tiny files.
Response:
Pass the file_id in subsequent attachments: [{file_id}] arrays. Uploads are private to the user who created them — a stolen file_id cannot be used from another account.
Pre-signed Upload URL (no-auth POST)
POST/api/files/reserve
Reserve a file_id and get a pre-signed upload URL that accepts a multipart POST with no auth header — the URL itself is the auth. The slot is single-use and expires in 5 minutes.
This is the path the MCP tool chamade_file_upload_url exposes. Useful when the uploader (a shell script, a CI job, a browser fetch) shouldn't see the API key.
Serve a File
GET/api/files/{token} · HEAD/api/files/{token}
Stream the bytes of an attachment. The token path param is the auth — it is 24 bytes of url-safe entropy (~128 bits), so possession of the URL grants read access. No X-API-Key header required. Makes it trivial to hand a URL to an external tool (a vision model, OCR service, etc.) without leaking your API key.
On a cache miss, Chamade re-fetches from the upstream platform (using the appropriate refetcher — Telegram getFile, Slack signed URL + bearer, Meta Graph media endpoint, NC Talk WebDAV GET, or plain HTTPS for Discord CDN). A concurrent lock per token dedupes re-fetches; if the platform has evicted the file, Chamade returns 410 Gone.
HEAD returns the same Content-Type, Content-Length, and Content-Disposition headers as GET without transferring bytes — useful for size probes.
Attachments in the Inbox
GET /api/inbox/{conversation_id} enriches each message with an attachments[] array:
Same shape on the push/long-poll events (dm_chat, call_chat) and on the MCP chamade_inbox tool. Your agent can curl the url directly — no additional auth header needed beyond X-API-Key when going through the authenticated path, or nothing at all when going through the opaque token path (see above).
