Video
Video generation is asynchronous. You submit a job, receive 202 Accepted immediately, then either poll for the result or receive a signed webhook when the job finishes.
/v1/video/generations202 · Idempotency-Key REQUIRED/v1/video/generations/{id}poll/v1/video/generations/{id}cancel (non-terminal) or delete (terminal)/v1/jobs/{id}kind-agnostic job viewThe job pipeline, state machine, idempotency, polling, cancellation and webhooks are fully implemented. Real video providers are not yet wired: unless your operator has enabled the mock media provider, submissions return 501 not_implemented with a structured error. Check GET /v1/models for models with modality: "video".
1. Submit a job
curl "https://api.smartapihub.com/v1/video/generations" \
-H "Authorization: Bearer $LLM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "vendor/video-model",
"prompt": "A slow aerial shot over a foggy pine forest at sunrise",
"duration_seconds": 8,
"aspect_ratio": "16:9",
"resolution": "1080p",
"audio": false,
"webhook_url": "https://api.example.com/hooks/llm-jobs",
"metadata": { "order_id": "4821" }
}'| Field | Type | Notes |
|---|---|---|
model | string, required | A modality: "video" model. |
prompt | string ≤ 10,000, required | |
duration_seconds | number 1–120 | Drives the cost estimate (per_video_second). |
image_url / end_image_url | https URL | First/last frame conditioning where supported. Must pass the SSRF guard. |
input_images | string[] ≤ 10 | Reference images (URLs or base64). |
resolution | string | e.g. 720p, 1080p — model-dependent. |
aspect_ratio | W:H | e.g. 16:9, 9:16. |
audio | boolean | Generate audio track where supported. |
webhook_url | https URL | Called on completion/failure. Public hosts only (SSRF guard). |
metadata | object of strings | Echoed back in the job. |
max_cost_micro | integer | Cap the reservation. Default reservation is 1.5 × estimate. |
routing | object | Routing override. |
Idempotency-Key is required
Video jobs are expensive, so the header is mandatory; omitting it returns 400 idempotency_key_required. Use a UUID or a stable identifier from your own system (≤ 255 chars). Re-submitting with the same key and body returns the existing job (202, Idempotent-Replayed: true) instead of creating a duplicate. Same key with a different body → 409 idempotency_conflict.
Response — 202 Accepted
{
"id": "0c1f4c2a-6b1e-4f6e-9a1e-2d7d0d8b3a11",
"object": "video.generation",
"status": "queued",
"model": "vendor/video-model",
"created_at": "2026-09-04T10:15:00Z",
"estimated_cost_micro": 400000
}estimated_cost_micro is reserved against your balance/budgets immediately; the reservation is settled to the actual cost when the job finishes and released if it fails, is cancelled or expires.
2. Poll for the result
curl "https://api.smartapihub.com/v1/video/generations/0c1f4c2a-6b1e-4f6e-9a1e-2d7d0d8b3a11" \
-H "Authorization: Bearer $LLM_API_KEY"{
"id": "0c1f4c2a-6b1e-4f6e-9a1e-2d7d0d8b3a11",
"object": "video.generation",
"request_id": "req_01J6ZK3M9PQR7S8T9V",
"org_id": "8a4e…",
"kind": "video",
"model": "vendor/video-model",
"provider": "vendor-provider",
"status": "succeeded",
"progress": 100,
"estimated_cost_micro": 400000,
"max_cost_micro": 600000,
"cost_micro": 384000,
"error": null,
"params": { "prompt": "A slow aerial shot…", "duration_seconds": 8, "aspect_ratio": "16:9" },
"artifacts": [
{
"id": "a3c9…",
"type": "video",
"content_type": "video/mp4",
"size_bytes": 18234567,
"width": 1920,
"height": 1080,
"duration_seconds": 8,
"url": "https://media.example.com/artifacts/…/a3c9.mp4?X-Amz-Expires=3600&…",
"expires_at": "2026-09-04T11:20:00Z"
}
],
"webhook_url": "https://api.example.com/hooks/llm-jobs",
"created_at": "2026-09-04T10:15:00Z",
"started_at": "2026-09-04T10:15:03Z",
"completed_at": "2026-09-04T10:19:41Z",
"expires_at": "2026-09-05T10:15:00Z"
}Poll every few seconds with backoff (e.g. 2 s → 30 s). progress (0–100) is provided when the provider reports it, otherwise null. When status is failed, error holds { code, message }.
Artifact URLs are signed and short-lived (about 1 hour by default; the exact instant is in expires_at). Fetching the job again issues fresh URLs. Artifacts themselves are retained for a limited period (default 7 days) and then deleted — download what you need.
GET /v1/jobs/{id} returns the same object for any job kind (video or music) — useful when you store job ids without their kind.
3. Cancel or delete
curl -X DELETE "https://api.smartapihub.com/v1/video/generations/0c1f4c2a-…" \
-H "Authorization: Bearer $LLM_API_KEY"- Job not terminal (
queued,submitted,running) → cancellation is requested at the provider, the job moves tocanceled, the cost reservation is released. Returns the job withstatus: "canceled". - Job terminal (
succeeded,failed,canceled,expired) → artifacts and the job record are deleted. Returns204 No Content. - If cancellation is impossible at that moment →
409 job_not_cancellable.
Job state machine
| Status | Meaning |
|---|---|
queued | Accepted and reserved; waiting for a worker. |
submitted | Sent to the provider. |
running | Provider is generating; progress may update. |
succeeded | Artifacts available; cost_micro settled. Terminal. |
failed | Provider or pipeline failure; error populated; reservation released. Terminal. |
canceled | Cancelled via DELETE. Terminal. |
expired | expires_at (default 24 h after creation) passed before completion. Terminal. |
Webhooks
When webhook_url is set, the gateway POSTs to it once the job reaches succeeded or failed:
POST /hooks/llm-jobs HTTP/1.1
Content-Type: application/json
X-LLM-Signature: sha256=3f2a9c…e41b
X-Request-Id: req_01J6ZK3M9PQR7S8T9V
{"event":"job.completed","job":{ …MediaJob as returned by GET… }}| Field | Values |
|---|---|
event | job.completed or job.failed |
job | The full job object, including artifacts[] with signed URLs |
Verifying X-LLM-Signature
The signature is sha256= followed by the hex HMAC-SHA256 of the raw request body using your organization's webhook secret (available in the dashboard). Always verify before trusting the payload, and compare in constant time.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
app.post('/hooks/llm-jobs', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.header('X-LLM-Signature') ?? '';
const expected = 'sha256=' + createHmac('sha256', process.env.LLM_WEBHOOK_SECRET!).update(req.body).digest('hex');
const ok = header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
if (!ok) return res.status(401).end();
const { event, job } = JSON.parse(req.body.toString('utf8'));
if (event === 'job.completed') {
// download job.artifacts[0].url before it expires
}
res.status(204).end();
});import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/hooks/llm-jobs")
def llm_jobs():
body = request.get_data() # raw bytes — do not re-serialise
expected = "sha256=" + hmac.new(os.environ["LLM_WEBHOOK_SECRET"].encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("X-LLM-Signature", ""), expected):
abort(401)
payload = request.get_json(force=True)
if payload["event"] == "job.completed":
pass # download payload["job"]["artifacts"][0]["url"] promptly
return "", 204Webhook delivery notes:
- Respond with any
2xxquickly; do heavy work asynchronously. - The webhook URL must be
httpsand resolve to a public address; private, loopback and cloud-metadata ranges are rejected at submit time (400 ssrf_blocked). - Treat webhooks as a hint and keep polling as a fallback for critical flows.
Errors
| HTTP | code | Cause |
|---|---|---|
| 400 | idempotency_key_required | Header missing. |
| 400 | validation_failed | Bad aspect_ratio, duration_seconds out of range, non-https URLs. |
| 400 | ssrf_blocked | webhook_url / image_url not allowed. |
| 402 | insufficient_credits / budget_exceeded / spending_limit_exceeded | Reservation exceeds balance or budget. |
| 404 | not_found | Unknown job id (or another organization's job). |
| 409 | idempotency_conflict | Key reused with a different body. |
| 409 | job_not_cancellable | DELETE while the job cannot be cancelled. |
| 501 | not_implemented | No real video provider is wired in this deployment. |