AI Video Generation
Generate short videos from a text prompt. The platform routes the request to one of three providers — Kling OmniVideo, Google Veo, or OpenAI Sora — and supports optional structured controls. When optional parameters are omitted, the selected model infers the best settings. When they are provided, they override the model's inference, subject to each provider's capability limits.
Video generation runs through the same message + streaming pipeline used by all AI capabilities: you send a message with capabilityKey: "ai.video.generate", then subscribe to an SSE stream that publishes progress and the final result.
Endpoints
| Method | Path | Description | Auth | Rate Limit |
|---|---|---|---|---|
| POST | /ai/sessions/:sessionId/messages | Send a message that triggers a video generation run | JWT + Entitlement | 20/min |
| GET | /ai/sessions/:sessionId/messages/:messageId/stream | SSE stream of progress and completion events | JWT + Entitlement | 20/min |
| POST | /ai/sessions/:sessionId/messages/:assistantMessageId/stop | Cancel an in-progress run | JWT + Entitlement | 30/min |
Generating a video requires the ai.video.generate entitlement on the active plan.
Provider Selection
| Provider | provider value | Strengths |
|---|---|---|
| Google Veo | veo | High fidelity, supported aspect ratios 16:9 and 9:16, fixed 8 s at 1080p / 4k |
| OpenAI Sora | sora | Up to 20 s, audio supported, image reference supported |
| Kling OmniVideo | kling | Aspect ratios 16:9, 9:16, 1:1; supports negative prompt, image and video references |
If provider is omitted, the platform-configured default provider is used. Explicit but unsupported values return a clear validation error.
Optional Parameters
All optional fields apply to the structured tool input embedded in the message. Send them at the top level of the request body alongside content and capabilityKey, or let the model infer when omitted.
| Field | Type | Allowed values | Notes |
|---|---|---|---|
provider | string | kling | veo | sora | Defaults to configured provider |
aspectRatio | string | 16:9, 9:16, 1:1 | Provider-dependent; 1:1 only on Kling |
duration | number | seconds | Sora ≤ 20 s; Veo at 1080p/4k = 8 s; Kling ∈ |
size | string | WIDTHxHEIGHT | Sora only; takes priority over aspectRatio |
resolution | string | 480p, 720p, 1080p, 4k | Veo and Sora |
negativePrompt | string | free-form | Kling only |
seed | number | integer | Where supported by the provider |
generateAudio | boolean | — | Sora and Kling |
inputImageUrl | string | image URL | Reference / starting frame |
inputVideoUrl | string | video URL | Reference clip (Kling) |
referenceImages | string[] | image URLs | Multiple references |
referenceVideos | string[] | video URLs | Multiple references (Kling) |
styleTokens | string[] | free-form hints | Lighting, mood, cinematic adjectives |
model | string | provider model key | Override the default model for the chosen provider |
metadata | object | free-form | Echoed back in the completion event for client correlation |
Aspect ratios 4:3, 3:4, and 21:9 are accepted by the schema for forward compatibility but are not yet exposed in the standard UI and will be dropped if the chosen provider does not support them.
POST /ai/sessions/:sessionId/messages
Send a user message that triggers a video generation run. The request returns immediately with the assistant message ID and a run ID; subscribe to the SSE stream to receive progress and the final video URL.
Request
Authentication: JWT Bearer token + active AI entitlement (ai.video.generate). Rate limit: 20/min.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Free-text prompt describing the desired video |
capabilityKey | string | Yes (for deterministic routing) | Set to ai.video.generate to skip intent detection |
provider | string | No | One of kling, veo, sora |
model | string | No | Provider-specific model identifier |
aspectRatio | string | No | One of 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 (provider-dependent) |
duration | number | No | Video duration in seconds (1–60). Numeric strings are accepted and coerced (e.g. "12" → 12). |
size | string | No | Exact pixel size as WIDTHxHEIGHT (Sora) |
resolution | string | No | 480p, 720p, 1080p, or 4k |
negativePrompt | string | No | What the video should avoid (Kling only); max 4000 chars |
seed | number | No | Random seed for reproducibility (integer; numeric strings coerced) |
generateAudio | boolean | No | Generate an audio track alongside the video |
inputImageUrl | string | No | http(s) URL of a reference / starting image |
inputVideoUrl | string | No | http(s) URL of a reference / starting video |
referenceImages | string[] | No | http(s) URLs of reference images (max 8) |
referenceVideos | string[] | No | http(s) URLs of reference videos (max 4) |
styleTokens | string[] | No | Stylistic hints (max 16 items, ≤ 64 chars each) |
metadata | object | No | Free-form metadata echoed in the completion event |
effortMode | string | No | basic, thinking, or pro — routes model selection |
idempotencyKey | string | No | Client-generated key to dedupe retries |
The authenticated request supplies the actor — request bodies never include accountId, chainerId, or workspaceId.
Response
{
"data": {
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"userMessage": {
"id": "550e8400-e29b-41d4-a716-446655440010",
"role": "user",
"content": "A serene sunset over snow-capped mountains, cinematic.",
"createdAt": "2026-05-22T10:00:00Z",
"toolCalls": []
},
"assistantMessage": {
"id": "550e8400-e29b-41d4-a716-446655440011",
"role": "assistant",
"content": "",
"createdAt": "2026-05-22T10:00:00Z",
"runId": "550e8400-e29b-41d4-a716-446655440012",
"status": "pending",
"toolCalls": [],
"modelName": "Veo",
"providerKey": "google"
},
"run": {
"id": "550e8400-e29b-41d4-a716-446655440012",
"status": "running"
},
"resolvedModel": {
"modelKey": "veo-2.0-generate-001",
"displayName": "Google Veo",
"provider": "google",
"reason": "explicit",
"requestedModelKey": null,
"missingCapabilities": []
}
}
}Error Responses
| HTTP | Meaning |
|---|---|
| 400 | Schema validation failed, or an unsupported provider value was sent |
| 401 | Missing or invalid JWT |
| 402 | Insufficient credits to reserve the requested duration |
| 400 | Capability-aware validation error — for example sending bpm with ai.video.generate returns bpm is only valid for ai.audio.generate in details.fields[0].message. See Messages: Capability Parameters. |
| 403 | Plan does not include ai.video.generate |
| 404 | sessionId not found |
| 429 | Rate limit exceeded |
Code Examples
curl -X POST "$BASE_URL/ai/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "A serene sunset over snow-capped mountains, cinematic.",
"capabilityKey": "ai.video.generate"
}'curl -X POST "$BASE_URL/ai/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Hand-drawn animation of a fox chasing leaves through a forest.",
"capabilityKey": "ai.video.generate",
"provider": "sora",
"aspectRatio": "9:16",
"resolution": "720p",
"duration": 12,
"generateAudio": true
}'curl -X POST "$BASE_URL/ai/sessions/$SESSION_ID/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Extend this clip with the same style for another 5 seconds.",
"capabilityKey": "ai.video.generate",
"provider": "kling",
"aspectRatio": "16:9",
"duration": 5,
"negativePrompt": "low quality, watermark",
"referenceImages": ["https://cdn.example.com/style-frame.png"]
}'const res = await fetch(
`${BASE_URL}/ai/sessions/${sessionId}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "A timelapse of a city skyline at golden hour.",
capabilityKey: "ai.video.generate",
provider: "veo",
aspectRatio: "16:9",
resolution: "1080p",
duration: 8,
}),
},
);
const { data } = await res.json();
const { assistantMessage, run } = data;
console.log("Run id:", run.id, "stream:", assistantMessage.id);import requests
res = requests.post(
f"{BASE_URL}/ai/sessions/{session_id}/messages",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
},
json={
"content": "A drone shot of waves crashing on a rocky shore.",
"capabilityKey": "ai.video.generate",
"provider": "sora",
"resolution": "720p",
"aspectRatio": "16:9",
"duration": 10,
},
)
body = res.json()["data"]
assistant_id = body["assistantMessage"]["id"]
run_id = body["run"]["id"]GET /ai/sessions/:sessionId/messages/:messageId/stream
Open a Server-Sent Events stream for an in-progress assistant message. The stream stays open for the lifetime of the run and closes shortly after the terminal event.
Request
Authentication: JWT Bearer token + active AI entitlement. Rate limit: 20/min.
Response
Event sequence — video generation
event: run.started
event: tool.started toolKey=media.video.generate
event: tool.progress message="Queued video generation..."
event: tool.progress message="Generating video..."
event: tool.progress message="Saving generated video..."
event: tool.completed toolKey=media.video.generate data={ videoUrl, ... }
event: message.delta (assistant prose summarising the result)
event: message.completed
event: run.settlement.completedThe number and timing of tool.progress events depend on the provider — stage labels are stable, percentages are not.
Event payload — tool.completed
{
"eventId": 6,
"type": "tool.completed",
"runId": "550e8400-e29b-41d4-a716-446655440012",
"stepId": "tc_abc123",
"timestamp": "2026-05-22T10:00:14Z",
"payload": {
"toolKey": "media.video.generate",
"callId": "tc_abc123",
"executionMs": 12480,
"success": true,
"data": {
"success": true,
"provider": "veo",
"model": "veo-2.0-generate-001",
"videoUrl": "https://media.chainabit.com/ai-generated/...mp4",
"thumbnailUrl": null,
"duration": 8,
"aspectRatio": "16:9",
"size": null,
"resolution": "1080p",
"storageKey": "ai/generated/video/...",
"externalJobId": "veo-op-1234",
"status": "completed"
}
}
}When the provider returns status: "pending" (typical for long-running jobs that exceed the polling window), poll the message via GET /ai/sessions/:sessionId/messages/:messageId to retrieve the final URL once available.
Code Examples
const stream = new EventSource(
`${BASE_URL}/ai/sessions/${sessionId}/messages/${assistantId}/stream`,
{ withCredentials: true },
);
stream.addEventListener("tool.progress", (event) => {
const { payload } = JSON.parse(event.data);
if (payload.activityType === "video_generation") {
console.log("progress:", payload.message);
}
});
stream.addEventListener("tool.completed", (event) => {
const { payload } = JSON.parse(event.data);
if (payload.toolKey === "media.video.generate") {
console.log("Video ready:", payload.data.videoUrl);
}
});
stream.addEventListener("run.settlement.completed", () => stream.close());import sseclient
import requests, json
response = requests.get(
f"{BASE_URL}/ai/sessions/{session_id}/messages/{assistant_id}/stream",
headers={"Authorization": f"Bearer {TOKEN}"},
stream=True,
)
client = sseclient.SSEClient(response)
for event in client.events():
if event.event == "tool.completed":
payload = json.loads(event.data)["payload"]
if payload["toolKey"] == "media.video.generate":
print("video:", payload["data"]["videoUrl"])
breakPOST /ai/sessions/:sessionId/messages/:assistantMessageId/stop
Cancel an in-progress video generation run. The partial state is finalised with status: "stopped". Reserved credits are released for runs that have not yet completed; runs that have already produced an output are kept and charged normally.
Request
Authentication: JWT Bearer token + active AI entitlement. Rate limit: 30/min.
Response
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440011",
"status": "stopped"
}
}Code Example
curl -X POST "$BASE_URL/ai/sessions/$SESSION_ID/messages/$ASSISTANT_ID/stop" \
-H "Authorization: Bearer $TOKEN"Behaviour Notes
- Backward compatibility: Sending
{ content, capabilityKey: "ai.video.generate" }alone still works. All structured parameters are optional. - Conflicts: When both
sizeandaspectRatioare provided to a provider that supportssize,sizewins; the conflict is recorded in the run's metadata. - Clamping: Out-of-range durations and unsupported sizes are clamped to provider-safe values where possible, otherwise rejected with a clear error.
- Storage: Generated videos are uploaded to the workspace's media bucket and returned as a time-limited URL. To refresh the URL after it expires, fetch the corresponding generation record via the message resource.
- Aspect ratios beyond
16:9,9:16,1:1are accepted by the schema for forward compatibility but are not yet exposed in default UI surfaces.
Related
- AI Messages — generic message / streaming pipeline used by all AI capabilities.
- AI Sessions — create the session that hosts a video run.
- Generative Media API — alternative, context-aware media pipeline driven by per-source consent.