API Documentation

Request formats, callbacks, and safe recovery for API integrations.

Bounded-funding release

These generation examples describe the bounded-funding API being prepared for coordinated rollout. Production verification is pending. Confirm access, credit-cap enforcement, and a commercially permitted workflow before automating paid work; an API key or code example is not production sign-off.

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/v1

Quick 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.

bash
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.

Header
Authorization: Bearer <YOUR_API_KEY>
Content Type
Content-Type: application/json

Security Best Practice

Never expose your API key in client-side code. Always make API calls from your server. Use environment variables to store keys and rotate them regularly.

Video Generation

Async Processing — Do NOT Hold Connections Open

Generation is asynchronous and can take several minutes. Submit a job with the required callback URL and poll for status as needed. Do NOT keep the submission connection open waiting for rendering. The response accepts a job, not a finished video.

Create a Video Job

POST/v1/video/generate

Submits 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

json
{
  "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

json
{
  "job_id": "job_01HXYZ",
  "status": "queued",
  "message": "Video generation job queued successfully",
  "estimated_seconds": 60,
  "credits_held": 150
}

Durable request identity

The required body 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

GET/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.

json
{
  "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 processing
escrowedCredits held; not yet rendering
moderatingContent checks in progress
processingGeneration in progress
renderingVideo is being rendered
publishingOutput is being published
succeeded / completedSuccessful terminal states; inspect video_url and warnings
failedTerminal failure — check error_message
degradedTerminal reduced-quality result; inspect warnings
canceledTerminal cancellation
refundedTerminal refund
flaggedTerminal moderation flag

Cancel a Job

POST/v1/video/jobs/{job_id}/cancel

Cancels 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.

bash
# 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

json
{
  "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 null
  • failedGeneration failed; error contains the reason
  • degradedReduced-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

The renderer can also send 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

javascript
// 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

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:

1st retry2 seconds
2nd retry4 seconds
3rd retry8 seconds

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.

MethodEndpointDescription
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)

javascript
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)

python
# 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 TierDefault Requests / Minute
Free100
Growth1000
Enterprise10000

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:

json
{
  "error": "Rate limit exceeded",
  "detail": "Maximum 100 requests per 60s. Retry after 60s.",
  "code": "RATE_LIMITED"
}
CodeMeaningDescription
400Bad RequestPrompt blocked by content moderation.
401UnauthorizedMissing or invalid API key.
402Payment RequiredInsufficient credits or credit escrow failed.
403ForbiddenAccess denied, for example by an API key IP restriction.
404Not FoundThe requested resource does not exist.
409ConflictJob is no longer in a cancellable state.
422Unprocessable EntityRequest body validation failed. Check the error details.
429Too Many RequestsRequest or spending limit exceeded. Honor Retry-After when present; inspect the error code.
500Internal Server ErrorInspect the response and recover the existing job before deciding whether a submission can be retried.
503Service UnavailableA submission or delivery acknowledgement can be uncertain. Reconcile the existing request identity; do not assume a refund.