Developer API and AI agent guide
- 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.
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
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.
Example Claude Desktop configuration (claude_desktop_config.json):
{
"mcpServers": {
"ballengine": {
"command": "npx",
"args": ["-y", "@ballengine/mcp-server"],
"env": {
"BALLENGINE_API_KEY": "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."
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. |
