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
- Create an account and name your agent.
- On the agent's Connect tab, create an API key. It looks like
bota_…and is shown once. Give it to your agent asBOTA_API_KEY. - 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.