# firstcall.dev > Meeting bot API for developers and AI agents. > Join Google Meet, Microsoft Teams, and Zoom meetings programmatically. > Real-time transcription, audio streaming, screenshots, and meeting participation. ## Pricing - Bot compute: $0.35/hour - Transcription (optional): $0.12/hour - Free tier: 360 minutes (6 hours), 1 concurrent bot - Pay-as-you-go with credits. No subscriptions. ## Authentication All API requests require a Bearer token: ``` Authorization: Bearer ak_live_xxxxxxxxxxxxxxxx ``` Get your API key at https://app.firstcall.dev/dashboard/api-keys ## API Base URL https://api.firstcall.dev ## Endpoints ### Create a bot POST /v1/bots Content-Type: application/json Request: ```json { "meet_url": "https://meet.google.com/abc-def-ghi", "bot_name": "My Bot", "mode": "audio-ws", "transcription": true } ``` Required fields: - meet_url (string): Google Meet, Teams, or Zoom URL (must be https) - bot_name (string): Display name in the meeting - mode (string): "audio-ws" | "webpage-audio" | "webpage-av" | "webpage-av-screenshare" Optional fields: - transcription (boolean): Enable real-time STT (default: false, costs $0.12/hr extra) - audio_streaming (boolean): Stream raw PCM audio to WebSocket (default: false) - webhook_url (string): Override account webhook for this bot's events - transcript_retention_hours (number): Hours to keep transcript for download (default: 24, max: 168 = 7 days) - audio_recording (boolean): Record meeting audio as WAV for post-call download (default: false) - recording_retention_hours (number): Hours to keep recording for download (default: 24, max: 168 = 7 days) Response (201): ```json { "bot_id": "bot-abc123def456", "status": "created", "ws_url": "wss://api.firstcall.dev/v1/bots/bot-abc123def456/ws", "created_at": "2026-03-21T14:30:01.123Z" } ``` ### List bots GET /v1/bots Response (200): ```json { "bots": [ { "bot_id": "bot-abc123", "meet_url": "https://meet.google.com/abc-def-ghi", "bot_name": "My Bot", "mode": "audio-ws", "platform": "google-meet", "status": "ready", "transcription": true, "created_at": "2026-03-21T14:30:01.123Z" } ] } ``` ### Get bot status GET /v1/bots/{bot_id} ### Stop a bot DELETE /v1/bots/{bot_id} Response (200): ```json { "bot_id": "bot-abc123", "status": "ending", "message": "Bot is leaving the meeting" } ``` ### Download transcript GET /v1/bots/{bot_id}/transcript GET /v1/bots/{bot_id}/transcript?format=text Available during and after the meeting (until retention expires). Only accessible by the bot's creator. Response (200, format=json): ```json { "bot_id": "bot-abc123", "meet_url": "https://meet.google.com/abc-def-ghi", "entry_count": 142, "entries": [ { "speaker": "Alice", "speaker_id": "p-1", "text": "Good morning everyone.", "timestamp": "2026-03-27T14:31:20.000Z" } ] } ``` Response (200, format=text): ``` [2:31:20 PM] Alice: Good morning everyone. [2:31:25 PM] Bob: Sure, I have the report ready. ``` ### Download recording GET /v1/bots/{bot_id}/recording Redirects to a presigned S3 URL (valid 1 hour) for the WAV audio recording. Audio format: WAV (PCM s16le, 16kHz, mono). Only available when `audio_recording: true` was set at bot creation. ## WebSocket — Real-Time Meeting Data Connect after creating a bot to receive live events: ``` wss://api.firstcall.dev/v1/bots/{bot_id}/ws?api_key={your_api_key} ``` ### Events you receive transcript.final — Completed utterance: ```json { "type": "transcript.final", "text": "The quarterly numbers look great.", "speaker": { "id": "p-1", "name": "Alice" }, "is_final": true, "timestamp": "..." } ``` transcript.partial — In-progress transcription: ```json { "type": "transcript.partial", "text": "The quarterly", "speaker": { "id": "p-1", "name": "Alice" }, "is_final": false, "timestamp": "..." } ``` meeting.participant_joined: ```json { "type": "meeting.participant_joined", "participant": { "id": "p-1", "name": "Alice" }, "participants": [...], "timestamp": "..." } ``` meeting.participant_left: ```json { "type": "meeting.participant_left", "participant": { "id": "p-2", "name": "Bob" }, "participants": [...], "timestamp": "..." } ``` meeting.active_speaker: ```json { "type": "meeting.active_speaker", "speaker": { "id": "p-1", "name": "Alice" }, "timestamp": "..." } ``` meeting.chat_message: ```json { "type": "meeting.chat_message", "sender": "Alice", "message": "Can everyone hear me?", "timestamp": "..." } ``` audio.chunk (requires audio_streaming: true): ```json { "type": "audio.chunk", "data": "base64-pcm-s16le-16khz-mono", "timestamp": "..." } ``` screenshot.result (after screenshot.take command): ```json { "type": "screenshot.result", "data": "base64-jpeg", "width": 1920, "height": 1080, "request_id": "req-1", "timestamp": "..." } ``` ### Commands you send Send audio into meeting: ```json { "type": "audio.send", "data": "base64-pcm-s16le-16khz-mono" } ``` Send chat message: ```json { "type": "meeting.send_chat", "message": "Hello everyone!" } ``` Raise hand: ```json { "type": "meeting.raise_hand" } ``` Leave meeting: ```json { "type": "meeting.leave" } ``` Take screenshot: ```json { "type": "screenshot.take", "request_id": "req-1" } ``` Start periodic capture: ```json { "type": "capture.start", "interval_ms": 1000 } ``` Stop capture: ```json { "type": "capture.stop" } ``` Start screenshare (mode webpage-av-screenshare only): ```json { "type": "screenshare.start", "url": "https://your-app.com/slides" } ``` Stop screenshare: ```json { "type": "screenshare.stop" } ``` ## Webhooks — Bot Lifecycle Events Configure webhooks at https://app.firstcall.dev/dashboard/settings (Webhooks section). You can register multiple webhook endpoints per account. Plan limits: - Free / starter plans: up to 3 webhooks - Pro / enterprise plans: up to 10 webhooks Each webhook has its own signing secret. Every event fans out in parallel to all enabled webhooks. Toggle and delete apply instantly; URL/name edits commit on Save All. Each webhook can be enabled/disabled independently. Events delivered to every enabled webhook: - bot.created — Bot was created - bot.status_changed — Bot changed status (starting, joining, waiting_room, initializing, ready, error, ended) - bot.ended — Final event with duration and cost breakdown - bot.transcript_ready — Transcript saved and ready to download (if transcription was enabled) - bot.recording_ready — Audio recording uploaded and ready to download (if audio_recording was enabled) bot.transcript_ready payload: ```json { "event": "bot.transcript_ready", "bot_id": "bot-abc123", "entry_count": 142, "download_url": "https://api.firstcall.dev/v1/bots/bot-abc123/transcript", "formats": ["json", "text"], "retention_hours": 24, "expires_at": "2026-03-28T14:30:05.000Z" } ``` ### Per-bot webhook override Set `webhook_url` at POST /v1/bots to override account-level fan-out for that single bot — events go ONLY to your override URL, signed with the account-level "Per-bot override secret" (separate from the per-row secrets of account webhooks; visible at the bottom of the Settings page). ### Signature contract Every webhook request includes these headers: - X-FirstCall-Signature: sha256=<64 lowercase hex chars> - X-FirstCall-Event: bot.created | bot.status_changed | bot.ended | bot.transcript_ready | bot.recording_ready - X-FirstCall-Event-Id: evt- (use for idempotency / dedup) - Content-Type: application/json Algorithm: HMAC-SHA256, hex-encoded, prefixed with `sha256=`. The HMAC key is the ENTIRE secret string from the dashboard, including the `whsec-` prefix. Don't strip it, don't base64-decode it. Pass the value as-is into your HMAC function. What is signed: the raw HTTP body bytes only. No timestamp, no msg-id prefix, no replay window. Idempotency via X-FirstCall-Event-Id. CRITICAL: Verifiers must HMAC the raw request body bytes, not a re-serialized version of the parsed JSON. Re-serialization can change key order, whitespace, or number formatting and break HMAC equality. Node.js (Express): use `express.raw({ type: 'application/json' })` on the webhook route only, then `req.body` is the raw Buffer to pass to HMAC. Python (Flask): `request.data` already returns raw bytes — works as-is. Example verification (Node.js): ```js const crypto = require('crypto'); app.post( '/webhooks/firstcall', express.raw({ type: 'application/json', limit: '1mb' }), (req, res) => { const sig = (req.headers['x-firstcall-signature'] || '').replace('sha256=', ''); const expected = crypto.createHmac('sha256', process.env.FIRSTCALL_WEBHOOK_SECRET) .update(req.body) // raw Buffer .digest('hex'); if (sig.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body.toString('utf8')); // ... handle event ... res.status(200).send('OK'); } ); ``` Retry policy: 3 retries with exponential backoff (1s, 2s, 4s) on 5xx or timeout. 4xx responses are not retried. Exhausted retries are persisted to a Redis-backed queue and replayed on backend restart. Each webhook delivery retries independently — one failing webhook does not block deliveries to your other configured webhooks. ## Bot Status Flow created → starting → joining → waiting_room* → initializing → ready → ended (* waiting_room only if meeting has one) End reasons: left (you stopped it), ended (meeting ended), rejected, blocked, error, timeout_silence, timeout_alone, timeout_waiting_room ## Bot Modes - audio-ws: Audio capture + injection. Lightest. For transcription bots, AI voice agents. - webpage-audio: Webpage with audio routing into meeting. - webpage-av: Webpage with audio + video injection. For slides, dashboards. - webpage-av-screenshare: Full AV + screenshare. Custom bot branding via webpage. ## Supported Platforms - Google Meet (https://meet.google.com/...) - Microsoft Teams (https://teams.microsoft.com/... or https://teams.live.com/...) - Zoom (https://zoom.us/j/...) ## Rate Limits - REST API: 100 requests/second per API key - WebSocket: 100 commands/second per connection - WebSocket payload: 1 MB max ## Error Codes - 400: Bad request (missing/invalid fields) - 401: Invalid API key - 402: Insufficient credits - 404: Bot not found - 429: Rate limit or concurrent bot limit exceeded - 500: Internal error ## Complete Node.js Example ```javascript const WebSocket = require('ws'); // 1. Create bot const res = await fetch('https://api.firstcall.dev/v1/bots', { method: 'POST', headers: { 'Authorization': 'Bearer ak_live_xxxxxxxx', 'Content-Type': 'application/json', }, body: JSON.stringify({ meet_url: 'https://meet.google.com/abc-def-ghi', bot_name: 'AI Assistant', mode: 'audio-ws', transcription: true, }), }); const { bot_id, ws_url } = await res.json(); // 2. Connect WebSocket const ws = new WebSocket(`${ws_url}?api_key=ak_live_xxxxxxxx`); ws.on('message', (data) => { const event = JSON.parse(data); if (event.type === 'transcript.final') { console.log(`[${event.speaker?.name}] ${event.text}`); } }); // 3. Stop when done await fetch(`https://api.firstcall.dev/v1/bots/${bot_id}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer ak_live_xxxxxxxx' }, }); ``` ## Complete Python Example ```python import requests import asyncio import json import websockets API_KEY = "ak_live_xxxxxxxx" # 1. Create bot r = requests.post("https://api.firstcall.dev/v1/bots", headers={"Authorization": f"Bearer {API_KEY}"}, json={"meet_url": "https://meet.google.com/abc-def-ghi", "bot_name": "AI Bot", "mode": "audio-ws", "transcription": True}) bot = r.json() # 2. Listen for events async def listen(): async with websockets.connect(f"{bot['ws_url']}?api_key={API_KEY}") as ws: async for msg in ws: event = json.loads(msg) if event["type"] == "transcript.final": print(f"[{event['speaker']['name']}] {event['text']}") asyncio.run(listen()) # 3. Stop requests.delete(f"https://api.firstcall.dev/v1/bots/{bot['bot_id']}", headers={"Authorization": f"Bearer {API_KEY}"}) ``` ## Full Documentation - REST API: https://firstcall.dev/docs/rest-api.md - WebSocket API: https://firstcall.dev/docs/websocket-api.md - Webhook API: https://firstcall.dev/docs/webhook-api.md - Dashboard: https://app.firstcall.dev