Developer docs

Everything an AI agent needs to send the bot to a meeting and read the notes back. Base URL: https://hot.bota.to/api/v1

Quickstart

  1. Create an account and name your agent.
  2. On the agent's Connect tab, create an API key. It looks like bota_… and is shown once. Give it to your agent as BOTA_API_KEY.
  3. Send the bot to a meeting:
curl -X POST https://hot.bota.to/api/v1/meetings \
  -H "Authorization: Bearer $BOTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"meeting_url": "https://meet.google.com/abc-defg-hij"}'

Then poll GET /meetings/{id} until the status is completed and read the notes.

Authentication

Send the agent's key as Authorization: Bearer bota_…. A key only sees its own agent's meetings.

curl https://hot.bota.to/api/v1/me -H "Authorization: Bearer $BOTA_API_KEY"

Send a bot

POST /meetings

Sends the bot into a Google Meet or Microsoft Teams meeting, now or at a time you choose. Zoom is coming soon: until then a Zoom link gets unsupported_meeting_url.

{
  "meeting_url": "https://meet.google.com/abc-defg-hij",   // required
  "join_at": "2026-10-22T15:00:00-07:00",                   // optional ISO 8601 with offset; omit to join now
  "title": "Design review",                                 // optional
  "metadata": { "conversation": "c_42" },                   // optional, returned as is (max 20 string pairs)
  "idempotency_key": "design-review-oct-22"                 // optional; or send an Idempotency-Key header
}

Replies 201 with the meeting, or 200 with the existing meeting when the same idempotency key, or the same link at about the same time, was already sent. Errors: invalid_meeting_url, unsupported_meeting_url, join_at_in_past, quota_exceeded (429), subscription_required (402: the account used its 3 free meetings and has no plan).

Read a meeting

GET /meetings/{id}

The meeting's status and, once it is completed, the notes. Poll it every 5 minutes while it isn't finished, or use a webhook.

curl https://hot.bota.to/api/v1/meetings/mtg_... \
  -H "Authorization: Bearer $BOTA_API_KEY"
{
  "id": "mtg_abc123",
  "object": "meeting",
  "status": "completed",
  "status_reason": null,
  "title": "Design review",
  "meeting_url": "https://meet.google.com/abc-defg-hij",
  "platform": "google_meet",
  "join_at": "2026-10-22T22:00:00.000Z",
  "created_at": "2026-10-21T08:12:44.000Z",
  "completed_at": "2026-10-22T23:07:10.000Z",
  "bot_state": "ended",
  "metadata": {},
  "tldr": "The team agreed to ship the beta on Nov 15.",
  "transcript_available": true,
  "notes": {
    "title": "Design review",
    "tldr": "The team agreed to ship the beta on Nov 15.",
    "summary": ["..."],
    "decisions": ["Ship the beta on Nov 15"],
    "action_items": [{ "task": "Send the deck", "owner": "Bob", "due": null }],
    "open_questions": [],
    "key_moments": [{ "timestamp": "12:30", "speaker": "Alice", "note": "..." }],
    "topics": ["beta"]
  }
}

List meetings

GET /meetings

Your meetings, newest first, without the notes (they carry tldr). Query: status=completed,failed, limit (1-100, default 20), cursor (the previous page's next_cursor).

{ "object": "list", "data": [ { "id": "mtg_...", "status": "completed", ... } ], "next_cursor": null }

Transcript

GET /meetings/{id}/transcript

The full transcript as JSON segments, or plain text with ?format=text ([00:03] Bob: Agreed.).

{ "object": "transcript", "live": false, "source": "soniox", "languages": ["en"], "segments": [{ "speaker": "Alice", "start_ms": 1000, "end_ms": 2500, "text": "..." }] }

Live meeting

While the bot is in the call, the transcript route returns what has been said so far ("live": true, from the platform's captions). Once the meeting is processed it returns the final transcript.

GET /meetings/{id}/chat

The meeting chat so far: { "data": [{ "sender": "Alice", "text": "@Hermes what did we decide?", "to": "everyone" | "bot", "at": "..." }] }

POST /meetings/{id}/chat

The bot writes to everyone in the chat: { "message": "We decided to ship on Nov 15." }. Both chat routes reply 409 not_in_call when the bot isn't in the meeting.

Ask it in the meeting

While the bot is in a call, anyone can type @name (the agent's name) and a question in the meeting chat. It answers from what has been said so far, or privately if you message it directly. @name leave sends it out of the call; the notes are still written.

You can switch the answers off in the agent's Settings; @name leave keeps working.

Webhooks

Set a URL in the agent's Settings and we POST an event when a meeting is ready or fails. A non-2xx answer is retried with backoff for about a day. The URL must be https and public.

POST https://your-agent.example.com/bota
Bota-Event: meeting.completed
Bota-Signature: t=1761034000,v1=<hex hmac>

{ "id": "whd_...", "object": "event", "type": "meeting.completed" | "meeting.failed",
  "created_at": "...", "data": { ...the meeting, with notes } }

Verify it: v1 is the hex HMAC-SHA256 of t + "." + rawBody with your signing secret. Reject old timestamps. Deliveries can repeat: use the event id to ignore duplicates.

import { createHmac, timingSafeEqual } from "node:crypto";

// header: the Bota-Signature header, rawBody: the request body exactly as received
function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

MCP

The same key works as an MCP server over HTTP at https://hot.bota.to/api/mcp. Tools: send_bot_to_meeting, list_meetings, get_meeting, get_transcript, read_meeting_chat, send_meeting_chat, cancel_meeting.

claude mcp add --transport http bota https://hot.bota.to/api/mcp --header "Authorization: Bearer $BOTA_API_KEY"

Or in a config file:

{
  "mcpServers": {
    "bota": {
      "url": "https://hot.bota.to/api/mcp",
      "headers": { "Authorization": "Bearer YOUR_BOTA_API_KEY" }
    }
  }
}

Cancel

DELETE /meetings/{id}

Cancels a meeting whose bot hasn't joined yet. Replies 409 (bot_in_call, not_cancellable) once it has.

Statuses

scheduled
The bot is booked to join at join_at.
joining
The bot is on its way into the meeting (it may be waiting to be admitted; see bot_state).
in_call
The bot is in the meeting, recording.
processing
The meeting ended; the recording is being transcribed and summarised.
completed
The notes and transcript are ready.
failed
No notes: see status_reason (the bot wasn't admitted, the link was refused, processing failed, ...).
cancelled
Cancelled before the bot joined.
missed
It was too late to join by the time the bot could be sent.

Errors and limits

Errors are JSON: {"error": {"code": "...", "message": "..."}} with a matching HTTP status. Each key may make 120 requests a minute (429 with Retry-After). An account can hold a limited number of live meetings and start a limited number a day.

To let an agent use the API on its own, give it an instruction like this (the dashboard's Connect tab has a ready-made one for your agent):

You have a meeting notetaker called "Hermes" (botato). Use its HTTP API: base URL https://hot.bota.to/api/v1, and send the header
"Authorization: Bearer $BOTA_API_KEY" with every request. POST /meetings sends the bot, GET /meetings/{id} returns status and notes.