# FirstCall REST API

The REST API is your starting point. Use it to create bots, check their status, and stop them.

**Base URL:** `https://api.firstcall.dev`

## Quick Start

Get your first bot running in 3 steps:

```bash
# 1. Create a bot
curl -X POST https://api.firstcall.dev/v1/bots \
  -H "Authorization: Bearer ak_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "meet_url": "https://meet.google.com/abc-def-ghi",
    "bot_name": "My Meeting Bot",
    "mode": "audio-ws",
    "transcription": true
  }'

# Response:
# {
#   "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"
# }

# 2. Connect to the WebSocket for real-time meeting data
wscat -c "wss://api.firstcall.dev/v1/bots/bot-abc123def456/ws?api_key=ak_live_xxxxxxxx"

# 3. Stop the bot when done
curl -X DELETE https://api.firstcall.dev/v1/bots/bot-abc123def456 \
  -H "Authorization: Bearer ak_live_xxxxxxxx"
```

## Authentication

All API requests require an API key in the `Authorization` header:

```
Authorization: Bearer ak_live_xxxxxxxxxxxxxxxx
```

Get your API key from the dashboard at [app.firstcall.dev/dashboard/api-keys](https://app.firstcall.dev/dashboard/api-keys).

### Authentication Errors

```json
// Missing header
{
  "error": "Missing Authorization header. Use: Bearer <api_key>"
}

// Invalid or revoked key
{
  "error": "Invalid or revoked API key"
}
```

## Endpoints

### Create a Bot

```
POST /v1/bots
```

Creates a new bot and sends it to join a meeting. The bot starts immediately — you'll receive status updates via your webhook.

**Request Body:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `meet_url` | string | Yes | Meeting URL (Google Meet, Teams, or Zoom) |
| `bot_name` | string | Yes | Display name for the bot in the meeting |
| `mode` | string | Yes | Bot mode (see modes below) |
| `transcription` | boolean | No | Enable real-time transcription (default: false) |
| `audio_streaming` | boolean | No | Stream raw audio to your WebSocket (default: false) |
| `webhook_url` | string | No | Override account webhook URL for this bot's events |
| `webpage_url` | string | Modes 2-4 | URL to load in the bot's webpage browser (required for modes `webpage-audio`, `webpage-av`, `webpage-av-screenshare`) |
| `screenshare_url` | string | No | URL to screenshare into the meeting (mode `webpage-av-screenshare` only). Can also be started later via WebSocket `screenshare.start` command. |
| `waiting_room_timeout` | number | No | Milliseconds to wait in waiting room before leaving (default: 180000 = 3 min). Set to 0 to disable. |
| `silence_timeout` | number | No | Milliseconds of no audio before leaving (default: 300000 = 5 min). Starts after a grace period. Set to 0 to disable. |
| `alone_timeout` | number | No | Milliseconds of being alone in meeting before leaving (default: 120000 = 2 min). Starts after a grace period. Set to 0 to disable. |
| `transcript_retention_hours` | number | No | Hours to keep the transcript for download (default: 24, min: 1, max: 168 = 7 days). Only applies when `transcription` is true. |
| `audio_recording` | boolean | No | Record meeting audio as WAV file for post-call download (default: false) |
| `recording_retention_hours` | number | No | Hours to keep the recording for download (default: 24, min: 1, max: 168 = 7 days). Only applies when `audio_recording` is true. |

**Bot Modes:**

| Mode | Description | Requires `webpage_url` | Use Case |
|------|-------------|----------------------|----------|
| `audio-ws` | Audio capture + injection via WebSocket | No | Transcription bots, AI voice agents |
| `webpage-audio` | Webpage with audio routing into meeting | **Yes** | Audio-only web apps |
| `webpage-av` | Webpage with audio + video injection | **Yes** | Slides, dashboards, custom avatar in meetings |
| `webpage-av-screenshare` | Full AV + screenshare capability | **Yes** | Complete meeting participation with screen sharing |

**Supported Platforms:**

| Platform | URL Pattern |
|----------|-------------|
| Google Meet | `https://meet.google.com/xxx-xxx-xxx` |
| Microsoft Teams | `https://teams.microsoft.com/l/meetup-join/...` or `https://teams.live.com/meet/...` |
| Zoom | `https://zoom.us/j/xxxxxxxxx` or `https://us05web.zoom.us/j/...` |

**Response (201 Created):**

```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"
}
```

The `ws_url` is the WebSocket endpoint for this bot. Connect to it with your API key to receive real-time meeting data (see [WebSocket API](websocket-api.md)).

**Error Responses:**

```json
// Missing required field
{ "error": "meet_url is required" }
{ "error": "bot_name is required" }

// Invalid mode
{ "error": "mode is required. Must be one of: audio-ws, webpage-audio, webpage-av, webpage-av-screenshare" }

// Unsupported platform
{ "error": "Could not detect platform from meet_url. Supported: Google Meet, Microsoft Teams, Zoom" }

// Insufficient credits
{
  "error": "Insufficient credits. Please recharge your account."
}

// Concurrent bot limit (free plan)
{
  "error": "Free plan allows 1 concurrent bot. Upgrade to remove limits."
}
```

---

### List Bots

```
GET /v1/bots
```

Returns all bots for your account (active and recent).

**Response (200 OK):**

```json
{
  "bots": [
    {
      "bot_id": "bot-abc123def456",
      "meet_url": "https://meet.google.com/abc-def-ghi",
      "bot_name": "Sales Call Bot",
      "mode": "audio-ws",
      "platform": "google-meet",
      "status": "ready",
      "reason": null,
      "transcription": true,
      "audio_streaming": false,
      "created_at": "2026-03-21T14:30:01.123Z"
    },
    {
      "bot_id": "bot-xyz789",
      "meet_url": "https://zoom.us/j/123456789",
      "bot_name": "Interview Bot",
      "mode": "audio-ws",
      "platform": "zoom",
      "status": "ended",
      "reason": "left",
      "transcription": true,
      "audio_streaming": false,
      "created_at": "2026-03-21T13:00:00.000Z"
    }
  ]
}
```

---

### Get Bot Details

```
GET /v1/bots/:bot_id
```

Returns detailed status and info for a specific bot.

**Response (200 OK):**

```json
{
  "bot_id": "bot-abc123def456",
  "meet_url": "https://meet.google.com/abc-def-ghi",
  "bot_name": "Sales Call Bot",
  "mode": "audio-ws",
  "platform": "google-meet",
  "status": "ready",
  "reason": null,
  "transcription": true,
  "audio_streaming": false,
  "created_at": "2026-03-21T14:30:01.123Z"
}
```

**Error Responses:**

```json
// Bot not found or belongs to another account
{ "error": "Bot not found" }
```

---

### Stop a Bot

```
DELETE /v1/bots/:bot_id
```

Makes the bot leave the meeting gracefully. The bot will finish any in-progress operations (like flushing final transcripts) before stopping.

**Response (200 OK):**

```json
{
  "bot_id": "bot-abc123def456",
  "status": "ending",
  "message": "Bot is leaving the meeting"
}
```

If the bot has already ended or errored:

```json
{
  "bot_id": "bot-abc123def456",
  "status": "ended",     // or "error"
  "message": "Bot already ended"
}
```

## Download Transcript

```
GET /v1/bots/:bot_id/transcript
GET /v1/bots/:bot_id/transcript?format=text
```

Download the meeting transcript. Available during the meeting (in-progress) and after it ends (stored for the configured retention period, default 24 hours).

**Query Parameters:**

| Param | Values | Default | Description |
|-------|--------|---------|-------------|
| `format` | `json`, `text` | `json` | Response format |

**Response (200 OK, format=json):**

```json
{
  "bot_id": "bot-abc123def456",
  "meet_url": "https://meet.google.com/abc-def-ghi",
  "bot_name": "Meeting Assistant",
  "started_at": "2026-03-27T14:30:01.000Z",
  "ended_at": "2026-03-27T15:15:22.000Z",
  "entry_count": 142,
  "entries": [
    {
      "speaker": "Alice",
      "speaker_id": "p-1",
      "text": "Good morning everyone, let's get started.",
      "timestamp": "2026-03-27T14:31:20.000Z"
    },
    {
      "speaker": "Bob",
      "speaker_id": "p-2",
      "text": "Sure, I have the quarterly report ready.",
      "timestamp": "2026-03-27T14:31:25.000Z"
    }
  ]
}
```

**Response (200 OK, format=text):**

```
Transcript: Meeting Assistant
Meeting: https://meet.google.com/abc-def-ghi
Started: 2026-03-27T14:30:01.000Z
Ended: 2026-03-27T15:15:22.000Z

[2:31:20 PM] Alice: Good morning everyone, let's get started.
[2:31:25 PM] Bob: Sure, I have the quarterly report ready.
```

**Error (404):**
- Bot not found, not owned by your account, transcription not enabled, or transcript expired

## Download Recording

```
GET /v1/bots/:bot_id/recording
```

Download the meeting audio recording as a WAV file. Redirects to a presigned S3 URL (valid for 1 hour). Available after the meeting ends and the recording is uploaded (usually within 30 seconds of bot disposal).

**Response (302 Redirect):** Redirects to a temporary S3 download URL.

**Audio format:** WAV (PCM s16le, 16 kHz, mono)

**Error (404):**
- Bot not found, not owned by your account, audio recording not enabled, recording not yet available, or expired
```

**Error Responses:**

```json
// Bot not found
{ "error": "Bot not found" }

// Internal error
{ "error": "Failed to stop bot" }
```

## Bot Lifecycle

After creating a bot, it goes through these states:

```
created → starting → joining → waiting_room* → initializing → ready → ended
                                                                  ↓
                                                                error
```

*`waiting_room` only occurs if the meeting has a waiting room enabled.

| Status | Description | What's Happening |
|--------|-------------|-----------------|
| `created` | Bot record created | API response returned |
| `starting` | Container launching | Docker image loading, browser starting |
| `joining` | Entering meeting | Navigating to meeting URL, clicking join |
| `waiting_room` | Waiting for host | Bot is in the meeting's waiting room |
| `initializing` | Setting up | In the call, configuring audio/video capture |
| `ready` | Fully active | Capturing audio, participants visible, transcription flowing |
| `error` | Failed | Something went wrong (see `reason` field) |
| `ended` | Complete | Bot left or meeting ended (see `reason` field) |

### End Reasons

| Reason | Description |
|--------|-------------|
| `left` | You called `DELETE /v1/bots/:id` — intentional stop |
| `ended` | The meeting ended on its own |
| `rejected` | Meeting host rejected the bot |
| `blocked` | Bot was blocked or kicked |
| `error` | Internal error (browser crash, network issue) |
| `timeout_silence` | No audio detected for too long |
| `timeout_alone` | Bot was alone in the meeting |
| `timeout_waiting_room` | Stuck in waiting room too long |

## Billing

- **Credits are checked** when you create a bot. If your balance is $0 or negative, the request is rejected with a 402 error.
- **Cost is deducted** when the bot ends (not during the meeting). Duration is calculated from bot creation to termination.
- **No mid-meeting kills.** If your credits run out during a meeting, the bot continues. Your balance may go negative. You just can't create new bots until you recharge.

### Rates

| Resource | Default Rate |
|----------|-------------|
| Bot compute | $0.35/hour |
| Transcription | $0.12/hour (additional, only if enabled) |
| Bot + Transcription | $0.47/hour |

Rates can be adjusted by account — check your dashboard for your current rates.

### Free Plan

- 6 free hours (360 minutes) of bot compute on signup
- 1 concurrent bot limit
- All features available

## Examples

### Node.js: Create a Transcription Bot

```javascript
const axios = require('axios');
const WebSocket = require('ws');

const API_KEY = 'ak_live_xxxxxxxxxxxxxxxx';
const BASE_URL = 'https://api.firstcall.dev';

async function main() {
  // 1. Create a bot with transcription
  const { data } = await axios.post(`${BASE_URL}/v1/bots`, {
    meet_url: 'https://meet.google.com/abc-def-ghi',
    bot_name: 'Notetaker',
    mode: 'audio-ws',
    transcription: true,
  }, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  console.log('Bot created:', data.bot_id);
  console.log('WebSocket URL:', data.ws_url);

  // 2. Connect WebSocket for real-time transcripts
  const ws = new WebSocket(`${data.ws_url}?api_key=${API_KEY}`);
  const transcript = [];

  ws.on('message', (raw) => {
    const event = JSON.parse(raw);

    switch (event.type) {
      case 'transcript.final':
        transcript.push({
          speaker: event.speaker?.name || 'Unknown',
          text: event.text,
          time: event.timestamp,
        });
        console.log(`[${event.speaker?.name}] ${event.text}`);
        break;

      case 'meeting.participant_joined':
        console.log(`${event.participant.name} joined (${event.participants.length} total)`);
        break;

      case 'meeting.participant_left':
        console.log(`${event.participant.name} left`);
        break;
    }
  });

  // 3. Stop after 1 hour
  setTimeout(async () => {
    await axios.delete(`${BASE_URL}/v1/bots/${data.bot_id}`, {
      headers: { Authorization: `Bearer ${API_KEY}` },
    });
    console.log('Bot stopped. Full transcript:', transcript);
  }, 60 * 60 * 1000);
}

main().catch(console.error);
```

### Python: Create a Bot and Listen for Events

```python
import requests
import asyncio
import json
import websockets

API_KEY = "ak_live_xxxxxxxxxxxxxxxx"
BASE_URL = "https://api.firstcall.dev"

def create_bot():
    """Create a bot via REST API."""
    response = requests.post(
        f"{BASE_URL}/v1/bots",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={
            "meet_url": "https://meet.google.com/abc-def-ghi",
            "bot_name": "Python Bot",
            "mode": "audio-ws",
            "transcription": True,
        },
    )
    response.raise_for_status()
    return response.json()

async def listen(bot_id):
    """Connect WebSocket and listen for events."""
    uri = f"wss://api.firstcall.dev/v1/bots/{bot_id}/ws?api_key={API_KEY}"

    async with websockets.connect(uri) as ws:
        print(f"Connected to bot {bot_id}")

        async for message in ws:
            event = json.loads(message)
            event_type = event.get("type")

            if event_type == "transcript.final":
                speaker = event.get("speaker", {}).get("name", "Unknown")
                print(f"[{speaker}] {event['text']}")

            elif event_type == "meeting.participant_joined":
                print(f"{event['participant']['name']} joined")

            elif event_type == "meeting.participant_left":
                print(f"{event['participant']['name']} left")

def stop_bot(bot_id):
    """Stop the bot."""
    response = requests.delete(
        f"{BASE_URL}/v1/bots/{bot_id}",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    print(f"Bot stopped: {response.json()}")

# Run
bot = create_bot()
print(f"Bot created: {bot['bot_id']}")
print(f"WS URL: {bot['ws_url']}")

asyncio.run(listen(bot["bot_id"]))
```

### cURL: Full Lifecycle

```bash
# Set your API key
export API_KEY="ak_live_xxxxxxxxxxxxxxxx"

# Create a bot (audio only, no transcription)
curl -s -X POST https://api.firstcall.dev/v1/bots \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "meet_url": "https://zoom.us/j/123456789",
    "bot_name": "Quick Bot",
    "mode": "audio-ws"
  }' | jq .

# List all your bots
curl -s https://api.firstcall.dev/v1/bots \
  -H "Authorization: Bearer $API_KEY" | jq .

# Get a specific bot's status
curl -s https://api.firstcall.dev/v1/bots/bot-abc123def456 \
  -H "Authorization: Bearer $API_KEY" | jq .

# Stop a bot
curl -s -X DELETE https://api.firstcall.dev/v1/bots/bot-abc123def456 \
  -H "Authorization: Bearer $API_KEY" | jq .
```

### Node.js: AI Voice Agent

```javascript
const axios = require('axios');
const WebSocket = require('ws');

const API_KEY = 'ak_live_xxxxxxxxxxxxxxxx';

async function startVoiceAgent(meetUrl) {
  // Create bot with transcription + audio streaming
  const { data } = await axios.post('https://api.firstcall.dev/v1/bots', {
    meet_url: meetUrl,
    bot_name: 'AI Assistant',
    mode: 'audio-ws',
    transcription: true,
    audio_streaming: true,
  }, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  const ws = new WebSocket(`${data.ws_url}?api_key=${API_KEY}`);

  ws.on('message', async (raw) => {
    const event = JSON.parse(raw);

    if (event.type === 'transcript.final') {
      // Process transcript with your AI
      const response = await yourAI.process(event.text, event.speaker?.name);

      if (response.shouldReply) {
        // Option A: Send a chat message
        ws.send(JSON.stringify({
          type: 'meeting.send_chat',
          message: response.text,
        }));

        // Option B: Inject AI-generated speech audio
        // const ttsAudio = await textToSpeech(response.text);
        // ws.send(JSON.stringify({
        //   type: 'audio.send',
        //   data: ttsAudio.toString('base64'),
        // }));
      }
    }

    if (event.type === 'audio.chunk') {
      // Raw audio for voice activity detection, speaker diarization, etc.
      const pcmBuffer = Buffer.from(event.data, 'base64');
      // Process audio...
    }
  });

  return data.bot_id;
}

startVoiceAgent('https://meet.google.com/abc-def-ghi');
```

### Node.js: Screenshot Bot

```javascript
const WebSocket = require('ws');

async function screenshotBot(botId) {
  const ws = new WebSocket(
    `wss://api.firstcall.dev/v1/bots/${botId}/ws?api_key=${API_KEY}`
  );

  ws.on('open', () => {
    // Take a single screenshot
    ws.send(JSON.stringify({
      type: 'screenshot.take',
      request_id: 'shot-1',
    }));

    // Start periodic capture every 2 seconds
    ws.send(JSON.stringify({
      type: 'capture.start',
      interval_ms: 2000,
    }));

    // Stop capture after 30 seconds
    setTimeout(() => {
      ws.send(JSON.stringify({ type: 'capture.stop' }));
    }, 30000);
  });

  ws.on('message', (raw) => {
    const event = JSON.parse(raw);

    if (event.type === 'screenshot.result') {
      // Save screenshot
      const buffer = Buffer.from(event.data, 'base64');
      require('fs').writeFileSync(`screenshot-${event.request_id}.jpg`, buffer);
      console.log(`Screenshot saved: ${event.width}x${event.height}`);
    }

    if (event.type === 'capture.frame') {
      console.log(`Frame ${event.frame_number} captured`);
    }
  });
}
```

## Rate Limits

| Resource | Limit |
|----------|-------|
| API requests | 100 requests/second per API key |
| WebSocket commands | 100 commands/second per connection |
| WebSocket payload | 10 MB max per message |
| Bot creation | Subject to credit and concurrent bot limits |

## HTTP Status Codes

| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Bot created successfully |
| 400 | Bad request (missing/invalid fields) |
| 401 | Unauthorized (invalid or missing API key) |
| 402 | Payment required (insufficient credits) |
| 404 | Bot not found |
| 429 | Too many requests (rate limit or concurrent bot limit) |
| 500 | Internal server error |
