API Documentation
Request formats, callbacks, and safe recovery for API integrations.
Bounded-funding release
Getting Started
The UltimateCreator AI API lets you generate videos and manage digital twin avatars programmatically. The generation endpoint below is prompt-only. All requests use the following base URL:
https://api.ultimatecreator.ai/v1Quick Start
Include your API key in the Authorization header as a Bearer token. Set UC_API_KEY on your server and replace the callback below with an endpoint you own on public HTTPS. First quote the same render fields with POST /v1/video/generate/quote. Review its returned credits before authorizing creation. The example ceiling of 150 credits is illustrative, not a tariff: replace it with the limit you approve. Save one idempotency key per logical request and reuse the unchanged request on retries.
curl --fail-with-body --max-time 30 -X POST https://api.ultimatecreator.ai/v1/video/generate \
-H "Authorization: Bearer $UC_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 746524ce-17c0-470a-9439-480093bf40b5" \
-d '{
"prompt": "A tech CEO delivering a keynote on stage",
"target_length_seconds": 30,
"max_credits": 150,
"webhook_url": "https://your-server.example/webhooks/video",
"idempotency_key": "746524ce-17c0-470a-9439-480093bf40b5"
}'Authentication
API keys are managed in the Portal dashboard. Keep keys on your server; revoke keys you no longer use.
Authorization: Bearer <YOUR_API_KEY>Content-Type: application/jsonSecurity Best Practice
Video Generation
Async Processing — Do NOT Hold Connections Open
Create a Video Job
/v1/video/generateSubmits an asynchronous video generation job. Required fields:prompt (1–5000 characters), target_length_seconds (integer, 5–300), webhook_url (resolvable public HTTPS), a saved idempotency_key, and max_credits (a caller-approved nonnegative integer). Optional aspect_ratioaccepts 16:9 (default), 9:16, or 1:1. This endpoint does not select an avatar, brand kit, resolution, style, or provider.
Request Body
{
"prompt": "A tech CEO delivering a keynote on stage",
"target_length_seconds": 30,
"max_credits": 150,
"webhook_url": "https://your-server.example/webhooks/video",
"aspect_ratio": "16:9",
"idempotency_key": "746524ce-17c0-470a-9439-480093bf40b5"
}Response — 202 Accepted
{
"job_id": "job_01HXYZ",
"status": "queued",
"message": "Video generation job queued successfully",
"estimated_seconds": 60,
"credits_held": 150
}Durable request identity
idempotency_keyidentifies one authorized job. Reuse it with the same render fields and max_credits after an uncertain submission; changed intent is a conflict, not a request to charge again. The examples also send the matching header for deployments that use it, but a header-cache expiry does not erase the job identity. Poll the existing job for current state. Do not create a new key merely because a response or callback was lost.Poll Job Status
/v1/video/jobs/{job_id}Returns the job status and a nullable video_url. Poll every 10–30 seconds with a bounded attempt count and a timeout on each HTTP request. Success is succeededor completed. Stop on other terminal states listed below rather than continuing indefinitely. Check HTTP errors before parsing a success response. A successful job can still have no download URL; inspect warnings and reconcile later rather than submitting another paid job.
{
"job_id": "job_01HXYZ",
"status": "succeeded",
"created_at": "2026-09-06T12:00:00Z",
"completed_at": "2026-09-06T12:04:12Z",
"video_url": "https://storage.googleapis.com/example/video.mp4?signature=example",
"error_message": null,
"placeholder_warning": null,
"lipsync_status": null,
"c2pa": {
"synthid": false,
"manifest_present": false,
"last_verified": null
},
"quality_scores": null,
"avatar_id": null
}Status Values
queuedJob received, waiting for processingescrowedCredits held; not yet renderingmoderatingContent checks in progressprocessingGeneration in progressrenderingVideo is being renderedpublishingOutput is being publishedsucceeded / completedSuccessful terminal states; inspect video_url and warningsfailedTerminal failure — check error_messagedegradedTerminal reduced-quality result; inspect warningscanceledTerminal cancellationrefundedTerminal refundflaggedTerminal moderation flagCancel a Job
/v1/video/jobs/{job_id}/cancelCancels jobs only in queued, escrowed, or moderating only when provider activity and settlement also permit cancellation. A queued status alone does not guarantee eligibility or a refund. Inspect the returned error and reconcile the account ledger. Successful cancellation is reported as canceled when polling the job. List your jobs with GET /v1/video/jobs.
# Set UC_JOB_ID to the job_id returned by generation.
curl --fail-with-body --max-time 30 -X POST \
"https://api.ultimatecreator.ai/v1/video/jobs/$UC_JOB_ID/cancel" \
-H "Authorization: Bearer $UC_API_KEY"Webhooks
The gateway sends status callbacks when jobs complete, fail, or finish with degraded output. Use callbacks as notifications and polling as the authoritative reconciliation path; delivery is not guaranteed and notifications can be duplicated.
Configuration
Configure your signing secret in the Portal dashboard under Settings → Webhooks. Supply webhook_url in every generation request, even if you plan to poll. Replace the example URL with an owned public HTTPS endpoint; localhost and private addresses are rejected. Store the configured signing secret asWEBHOOK_SECRET on your receiver.
Gateway Status Payload
{
"job_id": "job_01HXYZ",
"status": "completed",
"download_url": "https://storage.googleapis.com/example/video.mp4?signature=example"
}download_url can be null; use the polling endpoint to retrieve current state. A failed callback carries a top-level error string. Completed or degraded callbacks may also include degraded_stagesand quality_notes.
Gateway Status Values
completedGeneration completed; download_url may be nullfailedGeneration failed; error contains the reasondegradedReduced-quality terminal result; inspect optional quality notes
Signature Verification
These examples verify gateway status callbacks only. Their Ultimate-Signature header has format t=<unix>,v1=<hex>: HMAC-SHA256 of the timestamp, a dot, and the exact raw request bytes. Verify before JSON parsing; never reserialize the body. Reject missing, malformed, stale (over 300 seconds), or future timestamps and compare digests in constant time. Keep your server clock synchronized.
Supplemental Renderer Notifications
job.completedor job.failed events using a different Ultimate-Signature: sha256=... format and worker/global secret. They are not gateway status callbacks and intentionally fail these verifiers. Do not silently accept that alternate signature or treat it as authoritative; poll the job. If the gateway has no configured secret it can send unsigned callbacks, which your receiver must reject.Node.js
// Node.js ESM; npm install express
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const secret = process.env.WEBHOOK_SECRET;
if (!secret) throw new Error("Set WEBHOOK_SECRET from portal Settings");
const app = express();
function verifyWebhookSignature(rawBody, header, secret) {
if (!Buffer.isBuffer(rawBody) || typeof header !== "string") return false;
const match = /^t=([0-9]{1,12}),v1=([0-9a-f]{64})$/.exec(header);
if (!match || match[0] !== header) return false;
const age = Math.floor(Date.now() / 1000) - Number(match[1]);
if (age < 0 || age > 300) return false;
const expected = createHmac("sha256", secret)
.update(match[1] + ".")
.update(rawBody)
.digest();
const received = Buffer.from(match[2], "hex");
return received.length === expected.length &&
timingSafeEqual(received, expected);
}
// Register BEFORE express.json(); do not decompress or reserialize signed bytes.
app.post("/webhooks/video",
express.raw({ type: "application/json", limit: "1mb", inflate: false }),
(req, res) => {
if (!verifyWebhookSignature(req.body, req.get("Ultimate-Signature"), secret)) {
return res.status(401).json({ error: "Invalid gateway signature" });
}
let event;
try {
event = JSON.parse(req.body.toString("utf8"));
} catch {
return res.status(400).json({ error: "Invalid JSON" });
}
if (!event || typeof event.job_id !== "string" ||
!["completed", "failed", "degraded"].includes(event.status)) {
return res.status(400).json({ error: "Invalid gateway payload" });
}
console.log("Gateway status:", event.job_id, event.status);
return res.sendStatus(204);
}
);
// Place behind your public HTTPS reverse proxy.
app.listen(3000);Python
# pip install flask
import hashlib
import hmac
import json
import os
import re
import time
from flask import Flask, request
secret = os.environ["WEBHOOK_SECRET"]
if not secret:
raise RuntimeError("Set WEBHOOK_SECRET from portal Settings")
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1024 * 1024
def verify_webhook_signature(raw_body: bytes, header: str, secret: str) -> bool:
if not isinstance(header, str):
return False
match = re.fullmatch(r"t=([0-9]{1,12}),v1=([0-9a-f]{64})", header)
if not match:
return False
timestamp, signature = match.groups()
age = int(time.time()) - int(timestamp)
if age < 0 or age > 300:
return False
expected = hmac.new(
secret.encode("utf-8"),
timestamp.encode("ascii") + b"." + raw_body,
hashlib.sha256,
).digest()
received = bytes.fromhex(signature)
return len(received) == len(expected) and hmac.compare_digest(received, expected)
@app.post("/webhooks/video")
def handle_webhook():
raw_body = request.get_data()
if not verify_webhook_signature(
raw_body, request.headers.get("Ultimate-Signature", ""), secret
):
return {"error": "Invalid gateway signature"}, 401
try:
event = json.loads(raw_body)
except (ValueError, UnicodeDecodeError):
return {"error": "Invalid JSON"}, 400
if (not isinstance(event, dict)
or not isinstance(event.get("job_id"), str)
or event.get("status") not in ("completed", "failed", "degraded")):
return {"error": "Invalid gateway payload"}, 400
print("Gateway status:", event["job_id"], event["status"])
return "", 204
# Local smoke run only; deploy with a WSGI server behind public HTTPS.
if __name__ == "__main__":
app.run(host="127.0.0.1", port=3000)These receivers only log verified notifications. Before adding side effects, durably deduplicate by job ID and status, and persist or enqueue work before acknowledging with 2xx. Timestamp checks limit replay age but do not prevent duplicates within that window. Never acknowledge work you have not safely accepted.
Retry Policy & Dead Letter Queue
Gateway deliveries use a 15-second HTTP timeout. Failed deliveries receive up to three retries after the first attempt:
After four unsuccessful attempts, the gateway stores a Dead Letter Queue (DLQ) entry for operational investigation. This is not a delivery guarantee or a promise of portal replay. Poll the job if a callback is missing, and safely handle repeated notifications.
Avatars
Manage your digital twin avatars separately from prompt-only generation. Creating an avatar record does not immediately train it or attach it to /v1/video/generate.
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/avatars/ | List avatars owned by the authenticated user |
| POST | /v1/avatars/ | Create a pending avatar record |
| GET | /v1/avatars/{id} | Get an owned or public avatar |
| PATCH | /v1/avatars/{id} | Update name or visibility; public listing requires an active avatar |
| DELETE | /v1/avatars/{id} | Delete an avatar |
Code Examples
Server-side examples submit a video generation job and poll for its terminal state. Use the separate gateway webhook receivers above for callbacks. Set UC_API_KEY, UC_WEBHOOK_URL (your owned public HTTPS endpoint), and UC_IDEMPOTENCY_KEY (one saved UUID per logical job). Reuse that key if you rerun after an uncertain submission; use a new key only for a new job.
Node.js 20+ (native fetch, ESM)
import { setTimeout as sleep } from "node:timers/promises";
const API_BASE = "https://api.ultimatecreator.ai/v1";
const API_KEY = process.env.UC_API_KEY;
const WEBHOOK_URL = process.env.UC_WEBHOOK_URL;
const IDEMPOTENCY_KEY = process.env.UC_IDEMPOTENCY_KEY;
if (!API_KEY || !WEBHOOK_URL || !IDEMPOTENCY_KEY) {
throw new Error("Set UC_API_KEY, UC_WEBHOOK_URL, and UC_IDEMPOTENCY_KEY");
}
if (new URL(WEBHOOK_URL).protocol !== "https:") {
throw new Error("UC_WEBHOOK_URL must be your public HTTPS endpoint");
}
const headers = { Authorization: `Bearer ${API_KEY}` };
async function generateVideo() {
const createRes = await fetch(`${API_BASE}/video/generate`, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(30_000),
headers: {
...headers,
"Content-Type": "application/json",
"Idempotency-Key": IDEMPOTENCY_KEY,
},
body: JSON.stringify({
prompt: "A tech CEO delivering a keynote on stage",
target_length_seconds: 30,
max_credits: 150, // Replace with the credit ceiling you approved after quoting.
webhook_url: WEBHOOK_URL,
aspect_ratio: "16:9",
idempotency_key: IDEMPOTENCY_KEY,
}),
});
if (createRes.status !== 202) {
throw new Error(`Submit HTTP ${createRes.status}: ${await createRes.text()}`);
}
const { job_id } = await createRes.json();
if (typeof job_id !== "string" || !job_id) throw new Error("Missing job_id");
console.log("Job created:", job_id);
for (let attempt = 0; attempt < 60; attempt++) {
await sleep(15_000);
const response = await fetch(
`${API_BASE}/video/jobs/${encodeURIComponent(job_id)}`,
{ headers, redirect: "error", signal: AbortSignal.timeout(30_000) }
);
if (!response.ok) {
throw new Error(`Poll HTTP ${response.status}: ${await response.text()}`);
}
const job = await response.json();
console.log("Status:", job.status);
if (job.status === "succeeded" || job.status === "completed") {
if (!job.video_url) throw new Error("Job finished without a video URL; reconcile this job later");
console.log("Video URL:", job.video_url);
if (job.placeholder_warning) console.warn(job.placeholder_warning);
return job;
}
if (["failed", "degraded", "canceled", "refunded", "flagged"].includes(job.status)) {
throw new Error(`Job ${job_id}: ${job.status} — ${job.error_message ?? job.placeholder_warning ?? "inspect job details"}`);
}
}
throw new Error(`Polling limit reached for ${job_id}; resume checking this job, do not resubmit`);
}
generateVideo().catch((error) => {
console.error(error);
process.exitCode = 1;
});Python (requests)
# pip install requests
import os
import time
from urllib.parse import quote, urlparse
import requests
API_BASE = "https://api.ultimatecreator.ai/v1"
API_KEY = os.environ["UC_API_KEY"]
WEBHOOK_URL = os.environ["UC_WEBHOOK_URL"]
IDEMPOTENCY_KEY = os.environ["UC_IDEMPOTENCY_KEY"]
if not API_KEY or not IDEMPOTENCY_KEY:
raise ValueError("Set UC_API_KEY and UC_IDEMPOTENCY_KEY")
if urlparse(WEBHOOK_URL).scheme != "https" or not urlparse(WEBHOOK_URL).hostname:
raise ValueError("UC_WEBHOOK_URL must be your public HTTPS endpoint")
headers = {"Authorization": f"Bearer {API_KEY}"}
def generate_video():
create_res = requests.post(
f"{API_BASE}/video/generate",
headers={**headers, "Idempotency-Key": IDEMPOTENCY_KEY},
json={
"prompt": "A tech CEO delivering a keynote on stage",
"target_length_seconds": 30,
"max_credits": 150, # Replace with the credit ceiling approved after quoting.
"webhook_url": WEBHOOK_URL,
"aspect_ratio": "16:9",
"idempotency_key": IDEMPOTENCY_KEY,
},
timeout=30,
allow_redirects=False,
)
if create_res.status_code != 202:
raise RuntimeError(f"Submit HTTP {create_res.status_code}: {create_res.text}")
job_id = create_res.json()["job_id"]
if not isinstance(job_id, str) or not job_id:
raise RuntimeError("Missing job_id")
print("Job created:", job_id)
unsuccessful = {"failed", "degraded", "canceled", "refunded", "flagged"}
for _ in range(60):
time.sleep(15)
response = requests.get(
f"{API_BASE}/video/jobs/{quote(job_id, safe='')}",
headers=headers,
timeout=30,
allow_redirects=False,
)
if not 200 <= response.status_code < 300:
raise RuntimeError(f"Poll HTTP {response.status_code}: {response.text}")
job = response.json()
status = job["status"]
print("Status:", status)
if status in ("succeeded", "completed"):
if not job.get("video_url"):
raise RuntimeError("Job finished without a video URL; reconcile this job later")
print("Video URL:", job["video_url"])
if job.get("placeholder_warning"):
print("Warning:", job["placeholder_warning"])
return job
if status in unsuccessful:
reason = job.get("error_message") or job.get("placeholder_warning") or "inspect job details"
raise RuntimeError(f"Job {job_id}: {status} — {reason}")
raise TimeoutError(f"Polling limit reached for {job_id}; resume checking this job, do not resubmit")
if __name__ == "__main__":
generate_video()Rate Limits
Limits are applied per API key, using its configured rate_limit_per_minute, not the marketing subscription labels. The table shows defaults for newly created API keys; existing keys can differ. Inspect response headers for the effective request limit. A request-limit 429 includes Retry-After in seconds. Separate spending or endpoint limits can also return 429 without that header.
| API Key Tier | Default Requests / Minute |
|---|---|
| Free | 100 |
| Growth | 1000 |
| Enterprise | 10000 |
Rate Limit Headers
X-RateLimit-Limit — Maximum requests per window
X-RateLimit-Remaining — Requests remaining in current window
Retry-After — Seconds to wait when the request limiter rejects a call
Error Codes
Route errors use an envelope with ok: falseand error.code, error.message, error.details, and error.request_id. Middleware errors may instead be flat objects. Check the HTTP status first; do not assume every failure has the same JSON shape. A request-rate-limit response can look like this:
{
"error": "Rate limit exceeded",
"detail": "Maximum 100 requests per 60s. Retry after 60s.",
"code": "RATE_LIMITED"
}| Code | Meaning | Description |
|---|---|---|
| 400 | Bad Request | Prompt blocked by content moderation. |
| 401 | Unauthorized | Missing or invalid API key. |
| 402 | Payment Required | Insufficient credits or credit escrow failed. |
| 403 | Forbidden | Access denied, for example by an API key IP restriction. |
| 404 | Not Found | The requested resource does not exist. |
| 409 | Conflict | Job is no longer in a cancellable state. |
| 422 | Unprocessable Entity | Request body validation failed. Check the error details. |
| 429 | Too Many Requests | Request or spending limit exceeded. Honor Retry-After when present; inspect the error code. |
| 500 | Internal Server Error | Inspect the response and recover the existing job before deciding whether a submission can be retried. |
| 503 | Service Unavailable | A submission or delivery acknowledgement can be uncertain. Reconcile the existing request identity; do not assume a refund. |
Start with one video. Decide from the result.
Bring a script or brief. Check the available workflow and its cost before funding a render. Review the output before publishing.
Explore the StudioRendering time and feature availability vary by workflow.