Developer docs
- Aspect ratios
- 9:16 (Shorts/Reels/TikTok), 16:9 (YouTube), 1:1 (Square), 4:5, and 3:4.
- Your own music
- Compose to a mood such as dark phonk, synthwave or chill lofi, or use a track from your own library.
- MCP server
- A Model Context Protocol server for Claude Desktop, Cursor and other AI agents.
Three ways in, one set of settings
- Website player: the live simulator on your site with one iframe and a public key. Videos are made on your visitors' devices.
- Cloud render API: POST /api/v1/videos with a secret key, get an MP4. Paid with render credits.
- Claude and AI agents: the hosted MCP server, the OpenAPI spec or a ready prompt. Agents call the same API on your credits.
The player and the API read the same look settings with the same names: theme, palette, title_text, watermark_text, speed, ball_count, gravity, sound_effect and config_overrides. They go in the JSON body of a cloud render, in the player's URL, or in its set-settings message, and the same code applies them, so a look you design on your site renders the same in the cloud.
Authentication
Create a secret key on your Developer page and send it as a bearer token on every request. Keys are shown once at creation and can be revoked at any time. Keep them server-side: never ship a key inside a browser or mobile app.
Authorization: Bearer be_sk_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
API access is invite-only for now: our team switches it on for each account. Buying render credits does not turn it on.
Pricing
Rendering is billed with prepaid credits using linear pricing: 1 credit = 30 seconds of 720p 30 fps video, 2 credits = 30 seconds of 1080p 60 fps (at 720p 30 fps: 15s = 0.5 credits, 45s = 1.5 credits, 60s = 2.0 credits, 300s = 10.0 credits; double each at 1080p 60 fps). You can generate any clip length from 5 seconds up to 5 minutes (300 seconds). Clips shorter than 10 seconds are billed as 10 seconds.
Create a video
POST https://ballengine.app/api/v1/videos
Content-Type: application/json
Authorization: Bearer be_sk_...
{
"duration": 30,
"quality": "1080p60",
"aspect_ratio": "9:16",
"mode": "GHOST_ESCAPE",
"theme": "cyberpunk_neon",
"music_mood": "dark phonk",
"title_text": "Wait for the end!",
"watermark_text": "@MyChannel",
"speed": 1.2
}Parameters
| Field | Type | Description |
|---|---|---|
| duration | number | Duration in seconds (5 to 300, default: 30). Linear billing by quality: 1 credit per 30s at 720p30, 2 credits per 30s at 1080p60. |
| quality | string | "720p30" (720p at 30 fps, 1 credit per 30s, default) or "1080p60" (1080p at 60 fps, 2 credits per 30s). |
| aspect_ratio | string | "9:16", "16:9", "1:1", "4:5" or "3:4". The short edge is 720 at 720p30 and 1080 at 1080p60, so "9:16" is 720x1280 or 1080x1920. |
| mode | string | "GHOST_ESCAPE", "SPACE_SPIRAL", "RHYTHMIC", "ESCAPE", "DRAWER", "SPAWNER", "WAVE_GRID", "FLAG_BATTLE", "MARBLE_RUN_3D", or "RANDOM". |
| theme | string | "cyberpunk_neon", "dark_minimal", "retro_synthwave", "matrix_code", "gold_luxury", "fire_and_ice", "pastel_dream", "deep_space". |
| music_mood | string | Mood to compose in when no track is named (e.g. "dark phonk", "synthwave", "chill lofi", "epic battle"). |
| song | string | Name of a track in your own library, one you composed in the studio or uploaded yourself. |
| xmidi_token | string | Legacy catalogue token, kept so existing projects keep rendering. Not available for new work. Compose a track or upload your own instead. |
| title_text | string | On-screen header text overlay. |
| watermark_text | string | Custom creator watermark / social handle. |
| speed | number | Simulation speed multiplier (0.5 to 3.0, default: 1.0). |
| randomize_depth | string | Randomization depth: "LIGHT", "MODERATE", "DEEP" (default), "TOTAL", or "CUSTOM". |
| randomize_groups | string[] | Groups to randomize when depth is CUSTOM (e.g. ["ARENA", "TRAILS", "BALL_COLOR"]). |
| seed | string | 1-64 characters of [a-zA-Z0-9_-] for deterministic reproduction. |
| metadata | object | Up to 20 keys of your own (strings up to 500 characters, numbers or booleans). Returned on every read of the job and in its webhooks, so you can match a video to your records. |
Job response
HTTP 201 Created
{
"id": "job-k3x92ab1c",
"status": "queued",
"duration": 30,
"aspect_ratio": "9:16",
"quality": "1080p60",
"fps": 60,
"resolution": "1080x1920",
"mode": "GHOST_ESCAPE",
"seed": "f81kq2z9",
"credits_charged": 2,
"credits_remaining": 8.5,
"metadata": { "order_id": "A-1042" },
"created_at": 1753159000
}Poll for the result
GET https://ballengine.app/api/v1/videos/job-k3x92ab1c Authorization: Bearer be_sk_...
Poll every 10 to 15 seconds until status is "done". When ready, download_url provides a direct 15-minute presigned MP4 link.
HTTP 200 OK
{
"id": "job-k3x92ab1c",
"status": "done",
"duration": 30,
"resolution": "1080x1920",
"format": "mp4",
"download_url": "https://pub-...r2.dev/renders/job-k3x92ab1c.mp4?...",
"created_at": 1753159000
}Need a fresh link later? GET /api/v1/videos/:id/download answers with a redirect to a new 15-minute link for as long as the video is kept (7 days).
List, page and cancel
GET /api/v1/videos lists the jobs created with your key, newest first, 20 per page by default (up to 100 with limit). When has_more is true, pass next_starting_after as starting_after to get the next page. Filter with status (queued, rendering, done, failed), created_after and created_before (Unix seconds).
GET https://ballengine.app/api/v1/videos?limit=50&status=done&starting_after=job-k3x92ab1c
HTTP 200 OK
{ "jobs": [ ... ], "has_more": true, "next_starting_after": "job-p0q8m2x4a" }DELETE /api/v1/videos/:id cancels a queued job and refunds its credits in full. A job that has started rendering cannot be canceled (409 already_rendering); delete it once it has finished. Deleting a finished job removes the video file and the job.
DELETE https://ballengine.app/api/v1/videos/job-k3x92ab1c
HTTP 200 OK
{ "id": "job-k3x92ab1c", "deleted": true, "canceled": true, "credits_refunded": 1 }Webhooks: get notified instead of polling
Register an https URL on your Developer page or through the API. When a render finishes we send it a signed POST with the result. Any 2xx answer within 10 seconds counts as delivered. Anything else is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 24 hours. An endpoint that fails 20 times in a row is switched off until you turn it back on.
POST https://ballengine.app/api/v1/webhooks
Authorization: Bearer be_sk_...
{ "url": "https://your-server.com/ballengine/webhook", "events": ["video.completed", "video.failed"] }
HTTP 201 Created
{ "id": "we_3f9c...", "url": "...", "events": [...], "status": "active", "secret": "whsec_..." }The secret is returned once. Other calls: GET /api/v1/webhooks, DELETE /api/v1/webhooks/:id, and POST /api/v1/webhooks/:id/test to send a test event.
Limits and automatic switch-off
- Test events: 3 per minute and 15 per hour per account. Resends: 5 per minute and 30 per hour. Adding endpoints: 5 per minute and 20 per hour. Going over answers 429 with retry_after.
- An endpoint is switched off after 20 failed deliveries in a row, or after more than 200 deliveries in one hour.
- If test or resend calls keep coming after the limit (10 refused calls in an hour), every endpoint on the account is switched off.
- A switched-off endpoint receives nothing, test events included. Only you can turn it back on, by signing in to your Developer page. There is no API call for it, so a runaway script cannot undo its own switch-off.
What we send
POST https://your-server.com/ballengine/webhook
BallEngine-Event-Id: evt_job-k3x92ab1c_completed
BallEngine-Event-Type: video.completed
BallEngine-Signature: t=1753159120,v1=5d41402abc4b2a76b9719d911017c592...
{
"id": "evt_job-k3x92ab1c_completed",
"type": "video.completed",
"api_version": "v1",
"created_at": 1753159120,
"data": {
"video": {
"id": "job-k3x92ab1c",
"status": "done",
"duration": 30,
"resolution": "1080x1920",
"format": "mp4",
"seed": "f81kq2z9",
"metadata": { "order_id": "A-1042" },
"download_url": "https://ballengine.app/api/v1/videos/job-k3x92ab1c/download",
"created_at": 1753159000,
"finished_at": 1753159118
}
}
}A video.failed event carries "status": "failed", an "error" message and "credits_refunded" instead of a download link. The same event can arrive more than once, so use its id to ignore repeats. In the rare case a render reported as failed later finishes, you get a video.completed after the video.failed, and the later one is correct.
Check the signature (Node.js)
import crypto from 'node:crypto';
// rawBody: the request body exactly as received, before JSON parsing.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || '');
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}Connect Claude, ChatGPT or your own agent
Claude, ChatGPT, Cursor or your own agent can create videos for you. There are three ways to connect them:
- 1.
Copy and paste a prompt
Nothing to install. Copy the prompt from your Developer dashboard into Claude.ai or ChatGPT.
- 2.
OpenAPI 3.1 spec
Paste https://ballengine.app/api/v1/openapi.json into Custom GPTs, Make.com, Zapier, or LangChain.
- 3.
Hosted remote MCP
Point Claude Desktop or Cursor at https://ballengine.app/api/v1/mcp. Nothing to install locally.
The hosted MCP server has the whole platform as tools: generate_video, get_video_status, list_videos, cancel_video, estimate_video_cost, list_simulation_modes, list_visual_themes, list_look_settings, and for the website player get_embed_integration_guide, list_website_players, update_website_player (cloud video limits and websites) and create_cloud_render_token. Send your secret key as a bearer token.
Claude Desktop (claude_desktop_config.json), through the mcp-remote bridge:
{
"mcpServers": {
"ballengine": {
"command": "npx",
"args": [
"-y", "mcp-remote", "https://ballengine.app/api/v1/mcp",
"--header", "Authorization: Bearer ${BALLENGINE_API_KEY}"
],
"env": { "BALLENGINE_API_KEY": "be_sk_YOUR_API_KEY" }
}
}
}Claude Code, Cursor or any client that speaks MCP over HTTP:
claude mcp add --transport http ballengine https://ballengine.app/api/v1/mcp \ --header "Authorization: Bearer be_sk_YOUR_API_KEY"
Example prompts:
- "Claude, create a 30s 9:16 TikTok video with a neon cyberpunk theme, dark phonk music, and Ghost Escape mode."
- "Generate a 15s square video for Instagram with Space Spiral mode and a gold luxury theme."
- "Check the render status of job-k3x92ab1c."
- "Add the BallEngine player to my website, with a Cloud video button capped at 20 credits a month."
Website player
Show a BallEngine mode on your own website with one iframe. Visitors randomize it and download the video, made on their own device, counted against your site's monthly videos. The player runs in our frame, so your page's styles never reach it. The public key (be_pk_) only works on the websites we register for it. Invite-only: confirm interest on the developer platform page.
URL options: mode, duration (5 to 60 seconds), visitor (your user's id, for a per-user limit), and every look setting a cloud render takes (the shared settings), with config_overrides as URL-encoded JSON. The look stays on through every Randomize. Change it live with a set-settings message; the player answers settings-changed with the settings it applied and drops any it does not know.
Cloud videos from the player
Like the Studio, the player can offer an HD video made on our GPUs: switch on Cloud videos for the site in your console and the player shows a Cloud video button, or your page starts one with { type: 'cloud-render', quality?, duration? }. It renders the scene on screen and bills YOUR cloud credits (your plan's monthly credits, then bought ones; a paid plan is required), at the API's prices. You set the spend: credits a month for the site, videos a month per visitor (counted by visitor) and the longest length. The player answers cloud-queued, cloud-progress, cloud-done (with url, a signed MP4 link valid 15 minutes, for your server to keep) and cloud-failed (with a code; a failed render is refunded).
To give cloud videos only to the users you choose, such as your paying customers, switch on Only with a token from your server. The built-in button then hides, and each render needs a one-time token (10 minutes) your server mints with your secret key:
POST https://ballengine.app/api/v1/embed/cloud-tokens
Authorization: Bearer be_sk_...
{ "site_key": "be_pk_your_key", "visitor": "USER_ID" }
→ 201 { "token": "be_ct_...", "expires_at": 1761700000, "visitor": "USER_ID" }
// then, on your page:
player.contentWindow.postMessage(
{ target: 'ballengine', type: 'cloud-render', token, quality: '1080p60' },
'https://ballengine.app');For a longer or 16:9 video, render the same look from your server with POST /api/v1/videos. Never put a secret key in the page.
Let your AI do it: once invited, press Copy AI prompt on your Developer page and paste it into Claude, ChatGPT or Cursor in your website project. The same guide is at ballengine.app/byobe-guide.txt, and assistants connected to our MCP server get it from the get_embed_integration_guide tool.
<iframe
src="https://ballengine.app/embed?key=be_pk_your_key&mode=FLAG_BATTLE&visitor=USER_ID&duration=15&theme=cyberpunk_neon&title_text=Who%20escapes%20first%3F"
style="width: 100%; aspect-ratio: 9 / 16; border: 0"
></iframe>
<script>
const player = document.querySelector('iframe');
window.addEventListener('message', (e) => {
if (e.origin !== 'https://ballengine.app' || e.data?.source !== 'ballengine') return;
// ready (with e.data.modes for a mode picker) | randomized | mode-changed | settings-changed
// cloud-queued | cloud-progress | cloud-done (e.data.url) | cloud-failed
// export-progress | export-done | export-failed | limit-reached
if (e.data.type === 'export-done') upload(e.data.video); // the MP4 as a Blob
});
// Drive the player from your own buttons:
player.contentWindow.postMessage({ target: 'ballengine', type: 'randomize' }, 'https://ballengine.app');
// also { type: 'export' }, { type: 'set-mode', mode: 'RHYTHMIC' } and
// { type: 'set-settings', settings: { theme: 'gold_luxury', speed: 1.5 } }
</script>Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Invalid duration, aspect ratio, mode, or JSON schema. |
| 401 | invalid_api_key | Missing, malformed, unknown, or revoked key. |
| 402 | insufficient_credits | Balance too low. Buy credits on your Developer page. |
| 403 | api_access_disabled | Your account does not have API access enabled. |
| 404 | not_found | No such job or webhook endpoint for this account. |
| 409 | not_ready | The video has no file to download yet. Check its status first. |
| 409 | already_rendering | The video is rendering and cannot be canceled. Delete it once it has finished. |
| 409 | endpoint_disabled | The webhook endpoint is switched off. Turn it back on from your Developer page. |
| 429 | rate_limited / concurrency_limit | Too many requests (max 5 in-flight per key). |
| 503 | capacity / unavailable | Queue temporarily saturated, retry with exponential backoff. |
