Skip to content

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/... (no app_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.
MethodPathPurpose
POST/v1/{app_id}/chat/completionsChat completion (OpenAI-compatible)
POST/v1/{app_id}/embeddingsGenerate embeddings
GET/v1/{app_id}/ai/configGet AI configuration
PUT/v1/{app_id}/ai/configUpdate AI configuration
GET/v1/{app_id}/ai/usageGet AI usage statistics
MethodPathPurposeAuth
POST/v1/chat/completionsChat completionRequired
POST/v1/embeddingsGenerate embeddingsRequired
GET/v1/modelsList available models (OpenAI-shape)Required
GET/v1/public/modelsPublic model catalog with pricingNone

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.

POST /v1/chat/completions
Authorization: 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.

POST /v1/embeddings
Authorization: Bearer bb_sk_...
{
"model": "openai/text-embedding-3-small",
"input": "What is Butterbase?",
"encoding_format": "float"
}
GET /v1/models
Authorization: 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" }
]
}

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/models

No 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.

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 are returned in OpenAI-compatible shape:

{ "error": { "message": "...", "type": "...", "code": "..." } }
Statuserror.typeerror.codeWhen
401authentication_errormissing_credentialsNo Authorization header.
401authentication_errorinvalid_api_keyToken is unknown, revoked, or expired.
403permission_errorinsufficient_scopeAPI key is missing the ai:gateway scope.
402billing_errorinsufficient_creditsAccount balance is too low for the requested call.
404invalid_request_errormodel_not_foundRequested model id isn’t available.
400invalid_request_errorinvalid_requestRequest body failed validation.
5xxapi_error(varies)Temporary upstream issue. Retry with backoff.
POST /v1/{app_id}/chat/completions
Authorization: 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.

POST /v1/{app_id}/embeddings
Authorization: Bearer {token}
{
"model": "openai/text-embedding-3-small",
"input": "What is Butterbase?",
"encoding_format": "float"
}
ParameterDescription
modelEmbedding model ID (required)
inputString or array of strings (required)
encoding_format"float" (default) or "base64"
ModelIDDimensions
Text Embedding 3 Smallopenai/text-embedding-3-small1536
Text Embedding 3 Largeopenai/text-embedding-3-large3072
Text Embedding Ada 002openai/text-embedding-ada-0021536

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.

MethodPathPurpose
POST/v1/{app_id}/videos/completionsSubmit 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=NStream the rendered MP4

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.

curl:

Terminal window
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, requests
res = 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:

FieldRequiredDescription
modelyesVideo model ID (see “Choosing a model” above).
promptyesText description of the video to generate.
durationnoLength in seconds. Model-specific (commonly 4, 6, 8).
resolutionnoe.g. 720p, 1080p. Model-specific.
aspect_rationoe.g. 16:9, 9:16, 1:1. Model-specific.
generate_audionoBoolean. Some models can render audio alongside video.
seednoInteger for deterministic generation (not all providers honor this).
input_imagesnoArray of HTTPS image URLs for image-to-video / first-frame guidance.

Poll the URL from polling_url every 30 seconds until status is terminal (completed, failed, cancelled, or expired).

curl:

Terminal window
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, requests
def 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_urls is an array because some models render multiple variants. Use ?index=N to pick one.
  • charged_credits_usd is populated once the job settles (first terminal poll). It’s null while pending or in progress.
  • For status: "failed", error carries the upstream message.

The URLs in content_urls are absolute and require the same Bearer API key. They stream video/mp4 bytes.

curl:

Terminal window
curl -L "$CONTENT_URL" \
-H "Authorization: Bearer $BUTTERBASE_API_KEY" \
--output video.mp4

TypeScript:

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 requests
with 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)
StatusCodeMeaning
400USE_VIDEO_ENDPOINTYou sent a video model to /chat/completions; use /videos/completions instead.
400INVALID_INDEX?index= was not a non-negative integer.
402INSUFFICIENT_CREDITSNot enough credits to reserve the job. Response includes required_usd, available_usd, and your auto-refill state.
403FORBIDDENYou’re not the submitter of this job.
404MODEL_NOT_FOUNDUnknown model ID.
404JOB_NOT_FOUNDUnknown job ID, or the job belongs to a different app.
409JOB_NOT_COMPLETEDYou requested /content but the job hasn’t reached completed.
502MODEL_UNAVAILABLEUpstream temporarily unavailable. Retry.

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.

PUT /v1/{app_id}/ai/config
{
"defaultModel": "anthropic/claude-3.5-sonnet",
"maxTokensPerRequest": 4096,
"allowedModels": ["anthropic/claude-3.5-sonnet"]
}
FieldDescription
defaultModelModel used when none specified
maxTokensPerRequestToken limit per request (1-100,000)
allowedModelsRestrict allowed models
GET /v1/{app_id}/ai/usage?startDate=2026-01-01&endDate=2026-01-31

Response:

{
"totalTokens": 150000,
"totalCost": 0.45,
"byModel": {
"anthropic/claude-3.5-sonnet": {
"tokens": 120000,
"cost": 0.40,
"requests": 25
}
}
}

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-keys
Authorization: Bearer {jwt}
{
"name": "my-cli",
"scopes": ["ai:gateway"]
}

The response contains the plaintext key once — store it immediately. Subsequent requests show only the prefix.

ScopeGrants
*Full access to all Butterbase APIs the user can use.
ai:gatewayAccess 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.