AI API
Butterbase exposes an OpenAI-compatible API for chat completions, embeddings, and model listing. There are two ways to call it:
- App-scoped — calls go through
/v1/{app_id}/..., are billed to the app’s owner, and inherit the app’s AI configuration (default model, allowed models). - Gateway mode — calls go through
/v1/...(noapp_id), authenticated with a platform JWT or a personal API key. Use this when you want a generic OpenAI-compatible gateway and don’t need an app.
App-scoped endpoints
Section titled “App-scoped endpoints”| Method | Path | Purpose |
|---|---|---|
| POST | /v1/{app_id}/chat/completions | Chat completion (OpenAI-compatible) |
| POST | /v1/{app_id}/embeddings | Generate embeddings |
| GET | /v1/{app_id}/ai/config | Get AI configuration |
| PUT | /v1/{app_id}/ai/config | Update AI configuration |
| GET | /v1/{app_id}/ai/usage | Get AI usage statistics |
Gateway endpoints (no app required)
Section titled “Gateway endpoints (no app required)”| Method | Path | Purpose | Auth |
|---|---|---|---|
| POST | /v1/chat/completions | Chat completion | Required |
| POST | /v1/embeddings | Generate embeddings | Required |
| GET | /v1/models | List available models (OpenAI-shape) | Required |
| GET | /v1/public/models | Public model catalog with pricing | None |
Drop-in compatible with the OpenAI SDK: point baseURL at https://api.butterbase.ai/v1 and use a personal API key as the bearer token. The request and response shapes are identical to the app-scoped variants — only the path differs.
Chat completions (gateway)
Section titled “Chat completions (gateway)”POST /v1/chat/completionsAuthorization: Bearer bb_sk_...
{ "model": "anthropic/claude-3.5-sonnet", "messages": [ { "role": "user", "content": "Hello!" } ], "max_tokens": 500, "temperature": 0.7, "stream": false}Set "stream": true for server-sent events.
Embeddings (gateway)
Section titled “Embeddings (gateway)”POST /v1/embeddingsAuthorization: Bearer bb_sk_...
{ "model": "openai/text-embedding-3-small", "input": "What is Butterbase?", "encoding_format": "float"}List models
Section titled “List models”GET /v1/modelsAuthorization: Bearer bb_sk_...Response:
{ "object": "list", "data": [ { "id": "anthropic/claude-3.5-sonnet", "object": "model", "display_name": "Claude 3.5 Sonnet" }, { "id": "openai/gpt-4o", "object": "model", "display_name": "GPT-4o" } ]}Public model catalog
Section titled “Public model catalog”A separate unauthenticated endpoint returns the full catalog with pricing and context window — useful for documentation pages, model pickers, and tooling that needs to enumerate models before the user has signed in.
GET /v1/public/modelsNo authorization header required.
Response:
{ "models": [ { "id": "anthropic/claude-sonnet-4.6", "name": "Claude Sonnet 4.6", "inputPricePerMTokens": 3.6, "outputPricePerMTokens": 18.0, "contextWindow": 200000 }, { "id": "openai/gpt-4o", "name": "GPT-4o", "inputPricePerMTokens": 3.0, "outputPricePerMTokens": 12.0, "contextWindow": 128000 } ]}Prices are per 1 million tokens and reflect what your account is charged when you call the model. contextWindow may be null for models that don’t report it.
Authentication
Section titled “Authentication”Authenticate with either your platform JWT (for session-based clients like the dashboard) or a personal API key. Personal keys must have the ai:gateway scope to access these endpoints. See Personal API keys below.
Errors
Section titled “Errors”Errors are returned in OpenAI-compatible shape:
{ "error": { "message": "...", "type": "...", "code": "..." } }| Status | error.type | error.code | When |
|---|---|---|---|
| 401 | authentication_error | missing_credentials | No Authorization header. |
| 401 | authentication_error | invalid_api_key | Token is unknown, revoked, or expired. |
| 403 | permission_error | insufficient_scope | API key is missing the ai:gateway scope. |
| 402 | billing_error | insufficient_credits | Account balance is too low for the requested call. |
| 404 | invalid_request_error | model_not_found | Requested model id isn’t available. |
| 400 | invalid_request_error | invalid_request | Request body failed validation. |
| 5xx | api_error | (varies) | Temporary upstream issue. Retry with backoff. |
Chat completions
Section titled “Chat completions”POST /v1/{app_id}/chat/completionsAuthorization: Bearer {token}
{ "model": "anthropic/claude-3.5-sonnet", "messages": [ { "role": "system", "content": "You are a helpful assistant." }, { "role": "user", "content": "Hello!" } ], "max_tokens": 500, "temperature": 0.7, "stream": false}Standard OpenAI-compatible response format. Set "stream": true for server-sent events.
Embeddings
Section titled “Embeddings”POST /v1/{app_id}/embeddingsAuthorization: Bearer {token}
{ "model": "openai/text-embedding-3-small", "input": "What is Butterbase?", "encoding_format": "float"}| Parameter | Description |
|---|---|
model | Embedding model ID (required) |
input | String or array of strings (required) |
encoding_format | "float" (default) or "base64" |
Available embedding models
Section titled “Available embedding models”| Model | ID | Dimensions |
|---|---|---|
| Text Embedding 3 Small | openai/text-embedding-3-small | 1536 |
| Text Embedding 3 Large | openai/text-embedding-3-large | 3072 |
| Text Embedding Ada 002 | openai/text-embedding-ada-002 | 1536 |
Video generation
Section titled “Video generation”Video generation is asynchronous. You submit a job, poll for status, then download the bytes when it’s done. A single video typically takes 30 seconds to several minutes depending on model and length.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
| POST | /v1/{app_id}/videos/completions | Submit a generation job |
| GET | /v1/{app_id}/videos/completions/{job_id} | Poll job status (also downloads when terminal) |
| GET | /v1/{app_id}/videos/completions/{job_id}/content?index=N | Stream the rendered MP4 |
Choosing a model
Section titled “Choosing a model”The video models in your gateway appear in GET /v1/{app_id}/ai/models alongside chat and embedding models. Look for the ones whose IDs begin with provider prefixes for video families (e.g. bytedance/seedance-…, kwaivgi/kling-…, pixverse/…, google/veo-…). If you POST a video model to /chat/completions, Butterbase returns 400 USE_VIDEO_ENDPOINT with the correct URL in the message.
1. Submit a job
Section titled “1. Submit a job”curl:
curl -X POST "https://api.butterbase.ai/v1/$APP_ID/videos/completions" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/seedance-2.0-fast", "prompt": "A golden retriever running through sunflowers at sunset, cinematic", "duration": 4, "resolution": "720p", "aspect_ratio": "16:9" }'TypeScript (fetch):
const res = await fetch(`${BUTTERBASE_API_URL}/v1/${APP_ID}/videos/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'bytedance/seedance-2.0-fast', prompt: 'A golden retriever running through sunflowers at sunset, cinematic', duration: 4, resolution: '720p', aspect_ratio: '16:9', }),});const job = await res.json();// { job_id, status: 'pending', polling_url }Python (requests):
import os, requestsres = requests.post( f"{os.environ['BUTTERBASE_API_URL']}/v1/{APP_ID}/videos/completions", headers={"Authorization": f"Bearer {os.environ['BUTTERBASE_API_KEY']}"}, json={ "model": "bytedance/seedance-2.0-fast", "prompt": "A golden retriever running through sunflowers at sunset, cinematic", "duration": 4, "resolution": "720p", "aspect_ratio": "16:9", },)job = res.json()# {"job_id": "...", "status": "pending", "polling_url": "..."}Response (202 Accepted):
{ "job_id": "5cd4be3e-c65e-4524-97cf-4595a76e2096", "status": "pending", "polling_url": "https://api.butterbase.ai/v1/{app_id}/videos/completions/5cd4be3e-c65e-4524-97cf-4595a76e2096"}Request fields:
| Field | Required | Description |
|---|---|---|
model | yes | Video model ID (see “Choosing a model” above). |
prompt | yes | Text description of the video to generate. |
duration | no | Length in seconds. Model-specific (commonly 4, 6, 8). |
resolution | no | e.g. 720p, 1080p. Model-specific. |
aspect_ratio | no | e.g. 16:9, 9:16, 1:1. Model-specific. |
generate_audio | no | Boolean. Some models can render audio alongside video. |
seed | no | Integer for deterministic generation (not all providers honor this). |
input_images | no | Array of HTTPS image URLs for image-to-video / first-frame guidance. |
2. Poll for status
Section titled “2. Poll for status”Poll the URL from polling_url every 30 seconds until status is terminal (completed, failed, cancelled, or expired).
curl:
curl "https://api.butterbase.ai/v1/$APP_ID/videos/completions/$JOB_ID" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY"TypeScript:
async function poll(jobUrl: string, apiKey: string) { while (true) { const res = await fetch(jobUrl, { headers: { Authorization: `Bearer ${apiKey}` } }); const job = await res.json(); if (['completed', 'failed', 'cancelled', 'expired'].includes(job.status)) return job; await new Promise(r => setTimeout(r, 30_000)); }}Python:
import time, requestsdef poll(job_url, api_key): while True: job = requests.get(job_url, headers={"Authorization": f"Bearer {api_key}"}).json() if job["status"] in {"completed", "failed", "cancelled", "expired"}: return job time.sleep(30)Response (when completed):
{ "job_id": "5cd4be3e-c65e-4524-97cf-4595a76e2096", "status": "completed", "model": "bytedance/seedance-2.0-fast", "polling_url": "https://api.butterbase.ai/v1/{app_id}/videos/completions/5cd4be3e-...", "content_urls": [ "https://api.butterbase.ai/v1/{app_id}/videos/completions/5cd4be3e-.../content?index=0" ], "error": null, "created_at": "2026-05-24T09:59:10.738Z", "charged_credits_usd": 0.72576, "settled_at": "2026-05-24T10:00:56.769Z"}content_urlsis an array because some models render multiple variants. Use?index=Nto pick one.charged_credits_usdis populated once the job settles (first terminal poll). It’snullwhile pending or in progress.- For
status: "failed",errorcarries the upstream message.
3. Download the video
Section titled “3. Download the video”The URLs in content_urls are absolute and require the same Bearer API key. They stream video/mp4 bytes.
curl:
curl -L "$CONTENT_URL" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY" \ --output video.mp4TypeScript:
const mp4 = await fetch(job.content_urls[0], { headers: { Authorization: `Bearer ${apiKey}` } });const buf = Buffer.from(await mp4.arrayBuffer());fs.writeFileSync('video.mp4', buf);Python:
import requestswith requests.get(job["content_urls"][0], headers={"Authorization": f"Bearer {api_key}"}, stream=True) as r: with open("video.mp4", "wb") as f: for chunk in r.iter_content(8192): f.write(chunk)Error codes
Section titled “Error codes”| Status | Code | Meaning |
|---|---|---|
| 400 | USE_VIDEO_ENDPOINT | You sent a video model to /chat/completions; use /videos/completions instead. |
| 400 | INVALID_INDEX | ?index= was not a non-negative integer. |
| 402 | INSUFFICIENT_CREDITS | Not enough credits to reserve the job. Response includes required_usd, available_usd, and your auto-refill state. |
| 403 | FORBIDDEN | You’re not the submitter of this job. |
| 404 | MODEL_NOT_FOUND | Unknown model ID. |
| 404 | JOB_NOT_FOUND | Unknown job ID, or the job belongs to a different app. |
| 409 | JOB_NOT_COMPLETED | You requested /content but the job hasn’t reached completed. |
| 502 | MODEL_UNAVAILABLE | Upstream temporarily unavailable. Retry. |
Jobs survive across polls
Section titled “Jobs survive across polls”Each job is persisted. You can poll from any client / process — there’s no in-memory state. Lost the polling_url? It’s https://api.butterbase.ai/v1/{app_id}/videos/completions/{job_id}.
If you stop polling before the job completes, the credit reservation is automatically released after a few minutes and any upstream charge that occurred is on us. Just don’t expect to retrieve the video later — re-submit.
Image generation
Section titled “Image generation”Image generation follows the same async submit → poll → download shape as video. Most models complete in 5–30 seconds. Unlike /chat/completions, the image endpoint accepts image-native parameters (size, aspect_ratio, seed, n, reference images, masks) which the chat surface can’t express.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
| POST | /v1/{app_id}/images/completions | Submit a generation job |
| GET | /v1/{app_id}/images/completions/{job_id} | Poll job status |
| GET | /v1/{app_id}/images/completions/{job_id}/content?index=N | Stream the rendered PNG/JPEG |
Choosing a model
Section titled “Choosing a model”Image models appear in GET /v1/{app_id}/ai/models alongside chat and video. Available at launch:
| Family | Model IDs |
|---|---|
| OpenAI | openai/gpt-image-2, openai/gpt-image-1, openai/gpt-image-1-mini |
| Google (Nano Banana) | google/gemini-3-pro-image-preview, google/gemini-3.1-flash-image-preview, google/gemini-2.5-flash-image |
| ByteDance Seedream | bytedance/seedream-5-pro, bytedance/seedream-5-lite, bytedance/seedream-4-5 |
| Alibaba Wan | alibaba/wan-2.7-image-pro, alibaba/wan-2.7-image, alibaba/wan-2.6-t2i, alibaba/wan-2.6-image |
| PrunaAI | prunaai/p-image, prunaai/p-image-edit |
Each model exposes a different subset of parameters. Submitting an unsupported param returns 400 UNSUPPORTED_PARAM with the full whitelist of what that model does accept — no silent drops.
1. Submit a job
Section titled “1. Submit a job”curl:
curl -X POST "https://api.butterbase.ai/v1/$APP_ID/images/completions" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-image-2", "prompt": "A serene mountain lake at sunset, photorealistic", "size": "1024x1024", "n": 1 }'TypeScript (fetch):
const res = await fetch(`${BUTTERBASE_API_URL}/v1/${APP_ID}/images/completions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'google/gemini-3-pro-image-preview', prompt: 'A serene mountain lake at sunset, photorealistic', aspect_ratio: '16:9', }),});const job = await res.json();// { job_id, status: 'pending', polling_url }Python (requests):
import os, requestsres = requests.post( f"{os.environ['BUTTERBASE_API_URL']}/v1/{APP_ID}/images/completions", headers={"Authorization": f"Bearer {os.environ['BUTTERBASE_API_KEY']}"}, json={ "model": "alibaba/wan-2.7-image-pro", "prompt": "A cat wearing a spacesuit", "size": "2K", "seed": 42, },)job = res.json()# {"job_id": "...", "status": "pending", "polling_url": "..."}Response (202 Accepted):
{ "job_id": "0776b4a1-aef9-4fbc-8ac6-18255aa18614", "status": "pending", "polling_url": "https://api.butterbase.ai/v1/{app_id}/images/completions/0776b4a1-aef9-4fbc-8ac6-18255aa18614"}Request fields:
| Field | Required | Description |
|---|---|---|
model | yes | Image model ID (see table above). |
prompt | yes | Text description of the image to generate. |
size | no | e.g. 1024x1024, 1536x1024, or symbolic like 1K / 2K / 4K. Model-specific. |
aspect_ratio | no | e.g. 16:9, 1:1. Gemini and Pruna families only. |
n | no | Number of images to generate. GPT Image 2 / 1 / 1-mini only. |
seed | no | Integer for deterministic generation. Wan, Pruna, GPT Image 2, and a few OpenRouter models. |
input_images | no | Array of HTTPS image URLs for image-to-image or reference guidance (up to 14 refs on most models). |
mask | no | HTTPS URL of an alpha mask for edit mode. GPT Image 2 only. |
negative_prompt | no | Text description of what to avoid. Wan 2.6 only. |
provider | no | Object of model-specific params (e.g. Wan’s bbox_list / color_palette, GPT’s quality / background, Seedream’s optimize_prompt_options). Validated against the model’s provider-param whitelist. |
Image URL aliases: For convenience, top-level keys image, image_url, image_uri, reference_image, input_image, and starting_image are folded into input_images[] before validation. Only mask remains a distinct top-level field.
2. Poll for status
Section titled “2. Poll for status”Poll the URL from polling_url every 2–5 seconds until status is terminal (completed, failed, cancelled, or expired).
curl:
curl "https://api.butterbase.ai/v1/$APP_ID/images/completions/$JOB_ID" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY"TypeScript:
async function poll(jobUrl: string, apiKey: string) { while (true) { const res = await fetch(jobUrl, { headers: { Authorization: `Bearer ${apiKey}` } }); const job = await res.json(); if (['completed', 'failed', 'cancelled', 'expired'].includes(job.status)) return job; await new Promise(r => setTimeout(r, 2_000)); }}Python:
import time, requestsdef poll(job_url, api_key): while True: job = requests.get(job_url, headers={"Authorization": f"Bearer {api_key}"}).json() if job["status"] in {"completed", "failed", "cancelled", "expired"}: return job time.sleep(2)Response (when completed):
{ "job_id": "0776b4a1-aef9-4fbc-8ac6-18255aa18614", "status": "completed", "model": "openai/gpt-image-2", "polling_url": "https://api.butterbase.ai/v1/{app_id}/images/completions/0776b4a1-...", "content_urls": [ "https://api.butterbase.ai/v1/{app_id}/images/completions/0776b4a1-.../content?index=0" ], "error": null, "created_at": "2026-07-15T02:57:44.271Z", "charged_credits_usd": 0.010168, "settled_at": "2026-07-15T02:58:04.512Z"}content_urlsis an array — withn > 1on GPT Image, one entry per generated image.charged_credits_usdis populated once the job settles (first terminal poll). It’snullwhile pending or in progress.- For
status: "failed",errorcarries the upstream message.
3. Download the image
Section titled “3. Download the image”The URLs in content_urls are absolute and require the same Bearer API key. They stream PNG or JPEG bytes (see the Content-Type header — varies per model / request).
curl:
curl -L "$CONTENT_URL" \ -H "Authorization: Bearer $BUTTERBASE_API_KEY" \ --output image.pngTypeScript:
const img = await fetch(job.content_urls[0], { headers: { Authorization: `Bearer ${apiKey}` } });const buf = Buffer.from(await img.arrayBuffer());// Content-Type header tells you the format: image/png, image/jpeg, image/webpErrors
Section titled “Errors”Same taxonomy as video generation, plus one image-specific code:
| Status | Code | Meaning |
|---|---|---|
| 400 | UNSUPPORTED_PARAM | Request included a param the target model doesn’t accept. Response body lists supported_top_level and supported_provider for that model. |
| 400 | WRONG_MODALITY | Model isn’t an image model (probably a chat or video model). |
| 402 | INSUFFICIENT_CREDITS | App is out of credits. |
| 404 | JOB_NOT_FOUND | jobId doesn’t exist or belongs to another end-user. |
| 409 | JOB_NOT_COMPLETED | Requested /content on a job that hasn’t reached terminal status. |
| 502 | MODEL_UNAVAILABLE | Upstream temporarily unavailable. Retry. |
Notes on URL expiration
Section titled “Notes on URL expiration”Content URLs are backed by the upstream provider’s storage, which typically expires ~30 days after generation. If you need long-term storage, download the image and mirror it to your own bucket (Butterbase Storage works well) before the window closes.
Concurrency
Section titled “Concurrency”There is no per-app or per-key concurrency cap on the Butterbase side — fire off as many parallel POSTs as your credit balance covers, and poll each returned job_id independently. Upstream rate limits from the underlying model provider are the only ceiling; if you hit one, you’ll get a 429 with the specific upstream error surfaced.
Configuration
Section titled “Configuration”PUT /v1/{app_id}/ai/config
{ "defaultModel": "anthropic/claude-3.5-sonnet", "maxTokensPerRequest": 4096, "allowedModels": ["anthropic/claude-3.5-sonnet"]}| Field | Description |
|---|---|
defaultModel | Model used when none specified |
maxTokensPerRequest | Token limit per request (1-100,000) |
allowedModels | Restrict allowed models |
GET /v1/{app_id}/ai/usage?startDate=2026-01-01&endDate=2026-01-31Response:
{ "totalTokens": 150000, "totalCost": 0.45, "byModel": { "anthropic/claude-3.5-sonnet": { "tokens": 120000, "cost": 0.40, "requests": 25 } }}Personal API keys
Section titled “Personal API keys”To call the gateway endpoints from outside the dashboard (scripts, CLIs, the OpenAI SDK), mint a personal API key with the ai:gateway scope.
POST /api-keysAuthorization: Bearer {jwt}
{ "name": "my-cli", "scopes": ["ai:gateway"]}The response contains the plaintext key once — store it immediately. Subsequent requests show only the prefix.
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
* | Full access to all Butterbase APIs the user can use. |
ai:gateway | Access to POST /v1/chat/completions, POST /v1/embeddings, and GET /v1/models. Nothing else. |
The dashboard at /api-keys lists and revokes keys; for now, scoping a key to ai:gateway is done by calling POST /api-keys directly.
Meeting bots
Section titled “Meeting bots”ctx.ai.meetings lets your app spawn a bot that joins a Zoom, Google Meet, Microsoft Teams, or Webex call, records it, and transcribes the audio. Your app starts a bot via the REST API or SDK, then waits for webhook events to learn when the call ends and the artifacts (recording, transcript) are ready to download. Recordings and transcripts are billed against the app’s AI credit balance using the same ledger as chat completions.
Endpoints
Section titled “Endpoints”| Method | Path | Purpose |
|---|---|---|
| POST | /v1/ai/meetings | Start a bot |
| GET | /v1/ai/meetings/{bot_id} | Get bot status + artifact URLs |
| DELETE | /v1/ai/meetings/{bot_id} | Stop a bot |
| GET | /v1/ai/meetings | List bots |
| GET | /v1/ai/meetings/_estimate | Estimate cost up front |
Start a bot
Section titled “Start a bot”Request:
POST /v1/ai/meetingsAuthorization: Bearer bb_sk_...
{ "meetingUrl": "https://meet.google.com/abc-defg-hij", "transcript": true, "recording": "mp4", "metadata": { "dealId": "d_42" }}Request fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
meetingUrl | string | yes | — | Full URL of the meeting to join. |
transcript | boolean | no | true | Whether to generate a transcript. |
recording | "mp4" | "audio_only" | false | no | "mp4" | Recording format, or false to skip recording. |
metadata | Record<string, string> | no | {} | Arbitrary key–value pairs stored on the bot. Keys starting with bb_ are reserved and will be rejected with 400. |
Response (201 Created):
{ "id": "bot_01j9...", "status": "joining", "startedAt": null, "completedAt": null, "durationSeconds": null, "recordingUrl": null, "transcriptUrl": null, "metadata": { "dealId": "d_42" }}The MeetingBot shape is the same for all endpoints that return a bot.
Bot status lifecycle
Section titled “Bot status lifecycle”Bots move through these states in order:
| Status | Meaning |
|---|---|
joining | Bot is dialing into the meeting. |
waiting_room | Bot reached the meeting but is held in the waiting room. |
in_call | Bot has been admitted to the call. |
recording | Bot is actively recording (and transcribing, if enabled). |
ended | The call has ended; artifacts are being processed. |
done | Artifacts are ready. recordingUrl and transcriptUrl (if applicable) are now populated. |
fatal | Bot encountered an unrecoverable error. Check status for details and the bot.fatal webhook. |
SDK usage
Section titled “SDK usage”import { butterbase } from '@butterbase/sdk';
const bb = butterbase({ apiKey: '...', appId: '...' });
// Start a botconst { data: bot, error } = await bb.ai.meetings.start({ meetingUrl: 'https://meet.google.com/abc-defg-hij', transcript: true, recording: 'mp4', metadata: { dealId: 'd_42' },});
// Later, after the webhook tells you the bot is done:const { data: finished } = await bb.ai.meetings.get(bot!.id);// finished.recordingUrl + finished.transcriptUrl are now populatedAdditional SDK methods:
// Stop a bot earlyawait bb.ai.meetings.stop(bot!.id);
// List bots with optional filtersconst { data: bots } = await bb.ai.meetings.list({ status: 'done', limit: 20, cursor: '...' });
// Estimate cost before dispatchingconst { data: estimate } = await bb.ai.meetings.estimateCost({ durationMinutes: 60, transcript: true });// estimate.usd — projected total chargePricing
Section titled “Pricing”Recording: $0.50/hr + markup, prorated per second. Transcription: $0.15/hr + markup, also prorated per second. Both charges are applied to the app’s AI credit balance once the bot reaches done status.
Use GET /v1/ai/meetings/_estimate?durationMinutes=N&transcript=true to project the charge before dispatching a bot.
Webhook events
Section titled “Webhook events”Apps receive forwarded events at the URL configured via manage_ai’s configure_meetings_webhook action. Butterbase re-signs each forwarded event before delivery.
Event types:
| Event | When it fires |
|---|---|
bot.in_call_recording | Bot has been admitted and recording has started. |
bot.done | Bot left the call; recording artifact is available. |
bot.fatal | Bot failed; check bot status for details. |
recording.done | Recording artifact is ready to download. |
transcript.done | Transcript artifact is ready to download. |
transcript.failed | Transcription failed for this bot. |
Payload shape:
POST <your-forward-url>Content-Type: application/jsonx-bb-event: bot.donex-bb-signature: v1,<base64>x-bb-key-id: <16-char identifier>
{ "event": "bot.done", "data": { /* event-specific payload */ }}Signature & freshness
Section titled “Signature & freshness”The x-bb-signature header is an HMAC-SHA256 of the raw request body bytes, signed with Butterbase’s webhook signing key. A valid signature confirms the event originated from Butterbase’s infrastructure. Full per-tenant HMAC verification (where each app has its own signing key) is on the roadmap. For now, verify the request arrived over TLS from a Butterbase-owned origin, and confirm that x-bb-key-id matches the first 16 characters of the secret returned on the most recent rotate_secret call. If it does not match, the event was signed before your latest key rotation — treat it as stale and drop it.
Do not trigger side-effects (database writes, downstream calls) on duplicate deliveries. Use the event payload’s bot.id combined with the event name as an idempotency key. The upstream delivery service may retry for up to 24 hours.
Limits
Section titled “Limits”- Single region. In v1, bots are spawned from the US East workspace. Latency-sensitive multi-region routing is on the roadmap.
- No hard duration cap. Bots run until the call ends or the app’s AI credit balance is exhausted.
- Artifact retention. Recording and transcript artifacts are retained for 7 days after the call ends. Download them before the retention window expires.