# Bota API — Full Reference for AI Agents Fetch this document to understand the complete Bota API in one shot. No need to search individual pages. --- ## What Bota Is Bota is a multi-tenant hardware + API platform for capturing, transcribing, and summarizing offline conversations via AI wearable devices. It follows a B2B2C model: - **Your company** integrates the Bota API into your product's backend - **Your end users** wear Bota devices (Bota Pin, Bota Pin Pro) and record conversations - **Bota** stores audio, runs transcription (ASR), generates summaries, and delivers results via webhook - **End users never interact with Bota directly** — they interact with your product Typical verticals: healthcare (clinical documentation), sales (call coaching), legal (consultation recording), enterprise (meeting capture). Architecture: ``` End User with Device → BLE to Mobile App (or WiFi/4G direct) → Your Backend (calls Bota API with sk_live_*) → Bota API (stores audio, runs ASR, fires webhooks) → Your Backend (receives transcription.completed webhook) → Your Product (displays transcript/summary to end user) ``` --- ## Data Model ``` Organization (your company's Bota account) └── Project (isolated env; all API calls are project-scoped) ├── APIKey (sk_live_*, sk_test_*, rk_*, dtok_*, up_*) ├── WebhookEndpoint (your HTTPS endpoint for events) ├── EndUser eu_* (person who wears a device; no Bota login) │ └── Device dev_* (bound 1:1 to EndUser; unbound = reassignable) │ └── Recording rec_* (audio file + metadata) │ ├── Transcription txn_* (ASR result, speaker segments) │ └── Summary sum_* (LLM-generated from transcription) └── Device dev_* (unbound; not yet assigned to an EndUser) ``` **Key facts agents frequently get wrong:** - EndUsers are **not** Bota users. They have no Bota account, no API key, no login. They're subjects of recordings. - A Device can only be bound to **one EndUser at a time**. Unbind before reassigning. - `external_id` on EndUser is how you link a Bota EndUser to your own user system. Always set it. Use it to avoid duplicate creation. - Recording status flows: `pending` → `uploaded` → (transcription) `processing` → `completed` - Create the Recording record first, then upload audio. The record comes before the file. - The S3 upload goes directly to S3 — no Bota `Authorization` header on the PUT request. --- ## Authentication All requests use: `Authorization: Bearer ` ``` Key type Prefix Use case ───────────────────────────────────────────────────────────────────── Secret key sk_live_* / sk_test_* Server-side full project access Restricted key rk_live_* / rk_test_* Server-side limited scope access Device token dtok_* Issued on device bind; cellular devices use for direct upload Upload token up_* Single-use, expires in minutes ``` **Rules:** - Never expose `sk_*` or `rk_*` keys in mobile apps, frontend code, or repos - Store in `BOTA_API_KEY` env var - `sk_test_*` data is completely isolated from `sk_live_*` — separate environments - Device tokens (`dtok_*`) are issued once on bind, stored securely on device, rotated on unbind - When a device token is used, `device_id` and `end_user_id` are auto-populated from the token — do NOT include them in the request body --- ## Canonical Flows ### Flow 1 — Onboard an End User and Device ```bash # Step 1: Create end user (use external_id = your own user ID) POST /v1/end-users { "external_id": "user_123", "name": "Jane Smith", "email": "jane@example.com" } → { "id": "eu_abc123", "external_id": "user_123", ... } # Step 2: Register device (serial number from device packaging) POST /v1/devices { "serial_number": "SN-2025-001234" } → { "id": "dev_xyz789", "status": "unbound", "end_user_id": null, ... } # Step 3: Bind device to end user POST /v1/devices/dev_xyz789/bind { "end_user_id": "eu_abc123" } → { "id": "dev_xyz789", "status": "bound", "end_user_id": "eu_abc123", "device_token": "dtok_abc123..." ← store on device for cellular uploads; shown once } ``` **Gotchas:** - On 409 when creating end user, fetch by `external_id` and reuse — do not create a duplicate - `device_token` in the bind response is shown **once only** — store it securely on the device immediately - Device tokens are automatically revoked when the device is unbound --- ### Flow 2 — Upload a Recording (BLE / Mobile App path) ```bash # Step 1: Create recording record (before uploading audio) POST /v1/recordings { "device_id": "dev_xyz789", "end_user_id": "eu_abc123", ← optional if device is bound "started_at": "2025-01-15T09:00:00Z", "ended_at": "2025-01-15T09:30:00Z" } → { "id": "rec_abc123", "status": "pending", ... } # Step 2: Get presigned upload URL POST /v1/recordings/rec_abc123/upload-url { "content_type": "audio/wav" } → { "upload_url": "https://bota-uploads.s3.amazonaws.com/...", "headers": { "Content-Type": "audio/wav" }, "expires_at": "2025-01-15T10:05:00Z" } # Step 3: Upload directly to S3 (NO Authorization header — use only the headers from step 2) PUT https://bota-uploads.s3.amazonaws.com/... Content-Type: audio/wav [audio file bytes] # Step 4: Notify Bota upload is complete (triggers auto-processing if configured) POST /v1/recordings/rec_abc123/upload-complete → { "id": "rec_abc123", "status": "uploaded", ... } ``` **Gotchas:** - The S3 PUT uses **no** `Authorization: Bearer` header — only the headers returned in step 2 - Presigned URLs expire in ~15 minutes — get a fresh URL if upload hasn't started - `upload-complete` must be called after the S3 PUT — Bota doesn't detect the upload automatically - Use `Idempotency-Key` on POST /recordings to safely retry without duplicates --- ### Flow 3 — Transcribe and Receive via Webhook ```bash # Option A: Manual transcription (after upload-complete) POST /v1/transcriptions { "recording_id": "rec_abc123", "language": "en" } → { "id": "txn_def456", "status": "pending", "recording_id": "rec_abc123", ... } # Option B: Enable auto-transcription (recommended — see Flow 4) # No additional API call needed after upload-complete # Webhook delivers result (transcription typically takes 10–30% of audio duration) POST https://your-app.com/webhooks/bota { "id": "evt_ghi789", "type": "transcription.completed", "created_at": "2025-01-15T10:07:00Z", "data": { "id": "txn_def456", "recording_id": "rec_abc123", "status": "completed", "text": "Hello, thank you for coming in today. How can I help you?", "duration_seconds": 1847.5, "segments": [ { "start": 0.0, "end": 2.5, "text": "Hello, thank you for coming in today.", "speaker": "SPEAKER_00", "confidence": 0.95 }, { "start": 2.8, "end": 4.2, "text": "How can I help you?", "speaker": "SPEAKER_00", "confidence": 0.97 } ], "speakers": [ { "id": "SPEAKER_00", "label": "Speaker 1" }, { "id": "SPEAKER_01", "label": "Speaker 2" } ], "language": "en", "completed_at": "2025-01-15T10:07:00Z" } } ``` **Gotchas:** - Always verify the webhook signature before trusting the payload (see Flow 5) - Events may arrive out of order — do not assume `recording.uploaded` arrives before `transcription.completed` - Use the `id` field on the event for deduplication — the same event may be delivered more than once --- ### Flow 4 — Auto-Processing (zero-code pipeline) Enable once per project. Every recording uploaded in the project is automatically transcribed and optionally summarized. ```bash # Enable for entire project PUT /v1/projects/proj_xxx/config/processing { "auto_transcription": { "enabled": true }, "auto_summary": { "enabled": true, "template": "general_notes" } } # After this, upload-complete triggers the pipeline automatically: # recording.uploaded → transcription job queued → transcription.completed webhook # → summary job queued → summary.completed webhook ``` Available summary templates: `general_notes`, `sales_call`, `clinical_soap`, `legal_memo` **Config hierarchy** — can override at any level: ``` Organization (default) └── Project (override) └── EndUser (override) └── Device (override, most specific) ``` To override for a specific end user: ```bash PUT /v1/end-users/eu_abc123/config/processing { "auto_summary": { "enabled": false } ← this user gets transcription but no summary } ``` **Gotchas:** - Auto-processing is non-blocking — a processing failure never fails the upload - Requires AI provider keys to be configured (uses Bota's default keys unless you've configured your own) --- ### Flow 5 — Webhook Setup and Signature Verification ```bash # Register your webhook endpoint POST /v1/webhooks { "url": "https://your-app.com/webhooks/bota", "events": ["recording.uploaded", "transcription.completed", "summary.completed"] } → { "id": "wh_abc123", "secret": "whsec_xyz789..." ← save this; shown once } ``` **Verify signatures — Node.js (copy-paste ready):** ```javascript const crypto = require('crypto'); function verifyWebhookSignature(rawBody, signatureHeader, timestampHeader, secret) { if (!signatureHeader?.startsWith('v1=')) return false; const timestamp = Number(timestampHeader); const now = Math.floor(Date.now() / 1000); if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > 300) return false; const signatureHex = signatureHeader.slice('v1='.length); if (!/^[0-9a-f]{64}$/i.test(signatureHex)) return false; const signedPayload = `${timestamp}.${rawBody.toString('utf8')}`; const expected = crypto .createHmac('sha256', secret) .update(signedPayload, 'utf8') .digest(); const actual = Buffer.from(signatureHex, 'hex'); return expected.length === actual.length && crypto.timingSafeEqual(expected, actual); } // Express handler — must use raw body (express.raw), not parsed JSON app.post('/webhooks/bota', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-bota-signature']; const timestamp = req.headers['x-bota-timestamp']; if (!verifyWebhookSignature(req.body, sig, timestamp, process.env.WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } const event = JSON.parse(req.body.toString('utf8')); res.status(200).send('OK'); // respond immediately processAsync(event); // process after responding }); ``` **Verify signatures — Python (copy-paste ready):** ```python import hmac import hashlib import time def verify_webhook_signature( raw_body: bytes, signature_header: str, timestamp_header: str, secret: str ) -> bool: if not signature_header or not signature_header.startswith('v1='): return False try: timestamp = int(timestamp_header) except (TypeError, ValueError): return False if abs(int(time.time()) - timestamp) > 300: return False signature_hex = signature_header[len('v1='):] if len(signature_hex) != 64: return False signed_payload = str(timestamp).encode('utf-8') + b'.' + raw_body expected = hmac.new( secret.encode('utf-8'), signed_payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature_hex) @app.route('/webhooks/bota', methods=['POST']) def handle_webhook(): sig = request.headers.get('X-Bota-Signature') timestamp = request.headers.get('X-Bota-Timestamp') if not verify_webhook_signature( request.get_data(), sig, timestamp, os.environ['WEBHOOK_SECRET'] ): abort(401) event = request.json return 'OK', 200 ``` **Retry policy:** 6 attempts with exponential backoff (immediate, 1 min, 5 min, 30 min, 2 hr, 8 hr). **Gotchas:** - `X-Bota-Signature` uses the format `v1=` - Sign `.`, not the body alone - Use the **raw** request body for signature verification, not parsed JSON - Reject timestamps older than five minutes to limit replay attacks - Return 200 immediately and process async — a slow handler causes timeouts and retries - The webhook secret (`whsec_*`) is shown **once only** at creation time --- ## All Webhook Event Types ``` Event Trigger ──────────────────────────────────────────────────────── recording.created Recording record created via API recording.uploaded Audio upload completed (upload-complete called) recording.deleted Recording deleted transcription.started Transcription job started transcription.completed Transcription finished — includes full segments array transcription.failed Transcription failed — includes error.code and error.message summary.started Summary generation started summary.completed Summary finished — includes generated text summary.failed Summary failed device.low_battery Battery below 20% (enterprise) device.offline No sync for 24+ hours (enterprise) ``` Subscribe to all events: `"events": ["*"]` Subscribe to a category: `"events": ["transcription.*"]` --- ## Common Gotchas **Idempotency** Include `Idempotency-Key` on all POST requests that create resources or trigger jobs. Pattern: `rec_create_{userId}_{timestamp}`. The same key returns the same response without creating a duplicate. Different body + same key = 409 conflict. ``` Idempotency-Key: rec_create_user123_1705320000 ``` Supported on: POST /end-users, POST /devices, POST /devices/:id/bind, POST /recordings, POST /recordings/:id/upload-complete, POST /transcriptions, POST /summaries. **External IDs** Always set `external_id` on EndUser creation. On a 409 conflict, fetch by `external_id` and reuse — never create a duplicate. This is the primary key linking Bota to your system. ```bash GET /v1/end-users?external_id=user_123 ``` **Pagination** Use `cursor` from `next_cursor` in the response — not offset. `has_more: true` means more pages exist. ```bash GET /v1/recordings?limit=20 → { "data": [...], "has_more": true, "next_cursor": "eyJpZCI6InJlY18xMjMifQ" } GET /v1/recordings?limit=20&cursor=eyJpZCI6InJlY18xMjMifQ ``` Default sort: `created_at` descending (newest first). **Test vs Live** `sk_test_*` and `sk_live_*` data are completely isolated. Use separate projects for production, staging, and development. Test mode uses real ASR — transcription quality and timing match production. **S3 Upload** Do not include `Authorization: Bearer` on the S3 PUT. Use only the headers returned by the `/upload-url` endpoint. Presigned URLs expire in ~15 minutes. **Device Token** When creating recordings or uploading with a `dtok_*` token, do NOT include `device_id` or `end_user_id` in the request body — they are automatically extracted from the token and will be overridden if supplied. **Cascade deletes** Deleting an EndUser deletes all their recordings, transcriptions, and summaries. Deleting a Recording deletes its transcriptions and summaries. These are permanent. **Recording status** A recording must be in `uploaded` status before transcription can be triggered. Transcription will fail if called on a `pending` recording. --- ## Key API Endpoints Reference ``` Base URL: https://api.bota.dev/v1 End Users POST /end-users GET /end-users ?external_id=, ?limit=, ?cursor= GET /end-users/:id PATCH /end-users/:id DELETE /end-users/:id Devices POST /devices GET /devices ?status=, ?end_user_id=, ?limit=, ?cursor= GET /devices/:id PATCH /devices/:id POST /devices/:id/bind { end_user_id } → returns device_token (once) POST /devices/:id/unbind DELETE /devices/:id Recordings POST /recordings { device_id, end_user_id, started_at, ended_at, metadata } GET /recordings ?end_user_id=, ?device_id=, ?status=, ?limit=, ?cursor= GET /recordings/:id DELETE /recordings/:id Uploads POST /recordings/:id/upload-url { content_type } → { upload_url, headers, expires_at } POST /recordings/:id/upload-complete → triggers auto-processing GET /recordings/:id/download-url → { download_url, expires_at } Transcriptions POST /transcriptions { recording_id, language? } GET /transcriptions ?recording_id=, ?limit=, ?cursor= GET /transcriptions/:id Summaries POST /summaries { transcription_id, template? } or { transcription_id, prompt } GET /summaries ?transcription_id=, ?limit=, ?cursor= GET /summaries/:id Webhooks POST /webhooks { url, events[] } → { id, secret (once) } GET /webhooks DELETE /webhooks/:id Config (hierarchical) PUT /projects/:id/config/processing PUT /end-users/:id/config/processing PUT /devices/:id/config/processing ``` --- ## Error Response Format ```json { "error": { "code": "invalid_request", "message": "The 'external_id' field is required", "param": "external_id", "request_id": "req_abc123" } } ``` HTTP status codes: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable, 429 Rate Limited, 500 Server Error. Rate limits: 100 req/s, 1000 req/min. Headers: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. --- ## Skills for AI Coding Agents For opinionated, copy-paste-ready patterns for each integration flow, see the Bota Skills repository: - Device registration and binding: https://raw.githubusercontent.com/bota-dev/bota-skills/main/device-registration/skill.md - Recording upload: https://raw.githubusercontent.com/bota-dev/bota-skills/main/recording-upload/skill.md - Webhook handling: https://raw.githubusercontent.com/bota-dev/bota-skills/main/webhook-handling/skill.md - Auto-processing setup: https://raw.githubusercontent.com/bota-dev/bota-skills/main/auto-processing/skill.md - Streaming upload: https://raw.githubusercontent.com/bota-dev/bota-skills/main/streaming-upload/skill.md Full skills index: https://raw.githubusercontent.com/bota-dev/bota-skills/main/index.json