Complete Video API Standard
Unified 88API video generation standard covering references, keyframes, callbacks, polling, downloads, and errors.
This is the customer-facing standard for asynchronous video generation through 88API. Always use the public 88API model name and the public task id returned by the API.
All current public video models share the create, polling, and download lifecycle. Webhooks are available only for models that explicitly support them. Reference-media fields are model-family specific, however; do not apply one family's media example to every model.
Updated 2026-09-14. Reuse an accessible HTTPS reference directly, or upload a local file first. Then create a task, retrieve its result URL, and use that URL for playback, download, or another model's reference input. Storage support does not imply model support or automatic transcoding.
Task API (2026-09-15): SD2.0/SD2.5 (480P, 720P, 1080P), official Seedance and Mini, Wan3 (480p, 720p, 1080p), Kling Turbo, and MiniMax H3 use
POST /v1/videos. Save the returnedidand pollGET /v1/videos/{id}. Do not depend ontask_idor sendcallback_url: new requests for these models currently support polling only, and a nonempty callback URL is rejected before submission. Model names and authentication are unchanged. The old/v1/video/generationsendpoint is retained for transitional use on selected models only; new integrations must use/v1/videos.
Endpoints
| Purpose | Method and path |
|---|---|
| Create a video | POST https://88api.ai/v1/videos |
| Retrieve task status | GET https://88api.ai/v1/videos/{id} |
| Request an upload permit | POST https://88api.ai/v1/media/uploads |
| Upload file bytes | PUT {upload_url} with the returned upload headers |
| Download the result | GET {url} using the completed response's URL |
| Legacy archived-video redirect | GET https://88api.ai/v1/videos/{id}/content — not universal for every result |
| Receive a terminal callback | Set callback_url only for models that support webhooks |
Authorization: Bearer sk-your-api-key
Content-Type: application/json
Accept: application/jsonKeep API keys on your server. Never embed them in browser, mobile, or desktop client code.
Creating/querying tasks and requesting upload permits require Bearer authentication at 88api.ai. Uploads use only the scoped X-Media-Upload-Token and MIME header returned in the permit. Public assets.88api.ai media reads require no Bearer or cookies. Never forward your API key to media URLs, official result hosts, or redirect destinations. Anyone holding a public URL can read the media during its lifetime; task details remain authenticated.
Basic text-to-video request
curl --request POST 'https://88api.ai/v1/videos' \
--header 'Authorization: Bearer sk-xxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"model": "SD2.5 720P",
"prompt": "A silver sports car driving through a neon city at night, cinematic tracking shot",
"duration": 8,
"size": "16:9"
}'You may use an integer duration or a string seconds, for example "8".
Common request fields
| Field | Type | Required | Description |
|---|---|---|---|
model | string | yes | Exact public model name shown by 88API |
prompt | string | conditional | Prompt; only selected reference-capable models allow it to be empty |
negative_prompt | string | no | Negative prompt for multi-reference models; Veo uses metadata.negativePrompt |
duration | integer | no | Duration in seconds; mutually exclusive with seconds |
seconds | string | no | Duration as a string |
size | string | no | Aspect ratio or size, such as 16:9 or 1280x720 |
image | string | no | One reference image URL |
images | string[] | no | Reference image URLs; recommended image form |
video / videos | string / string[] | no | Gemini Omni requires these top-level fields; multi-reference families use metadata.referenceVideos |
seed | integer | no | Random seed |
generate_audio | boolean | no | Generate audio on models that explicitly support it |
camera_control | object | no | Model-specific camera control |
callback_url | string | no | Only for models that support webhooks; unavailable for the polling models listed above |
metadata | object | no | Video/audio references, keyframes, and other extensions |
Output resolution is fixed by the public model name. Do not attempt to change a model tier by overriding resolution.
Reference media
Reference images
Use the top-level images array for consistent behavior across current models:
{
"model": "SD2.5 720P",
"prompt": "Keep the same character identity and clothes, walking by the sea",
"duration": 10,
"size": "16:9",
"images": [
"https://cdn.example.com/character-front.jpg",
"https://cdn.example.com/character-side.jpg"
]
}Reference videos and audio for multi-reference models
For the SD, Seedance, H3, Wan, and Kling families that support multiple references, put video and audio references in metadata:
{
"model": "SD2.5 720P",
"prompt": "Use the product appearance, camera motion, and sound rhythm from the references",
"duration": 12,
"size": "16:9",
"images": ["https://cdn.example.com/product.jpg"],
"metadata": {
"referenceVideos": [
"https://cdn.example.com/camera-motion.mp4"
],
"referenceAudios": [
"https://cdn.example.com/sound-reference.mp3"
]
}
}Do not submit multiple aliases for the same media type. For example, do not send both top-level images and metadata.referenceImages; compatibility aliases are selected, not guaranteed to be merged.
H3 reference types and limits depend on the service currently available to your account. Do not assume that all routes behind a public model name have identical capabilities.
First and last frames
{
"model": "Seedance-2.5-720p官方版",
"prompt": "Smoothly transition from the morning frame to the night frame",
"duration": 10,
"size": "16:9",
"metadata": {
"firstFrame": "https://cdn.example.com/start.jpg",
"lastFrame": "https://cdn.example.com/end.jpg"
}
}lastFrame requires firstFrame. For models marked as exclusive below, keyframes cannot be mixed with ordinary image, video, or audio references. Veo instead uses ordered images with metadata.video_mode, as described below.
Media URL requirements
- Use directly downloadable public HTTPS URLs for reference video/audio; local paths,
blob:anddata:video/...are not a portable reference-video protocol. Upload local files first. - URLs must not require cookies, login state, Referer, or custom headers.
- Do not use localhost, private IPs, container hostnames, or intranet-only URLs.
- Keep URLs valid for at least 60 minutes after task creation.
- Recommended formats: JPG/PNG/WebP, MP4/MOV, and MP3/WAV.
- Keep each reference video below 50 MB and each reference audio file below 15 MB when possible.
- Only submit material you are authorized to use.
Local media: permit, upload, reference
Send JSON to POST /v1/media/uploads with your server-side Bearer key:
{
"size": 1234567,
"mime_type": "video/mp4",
"sha256": "<SHA-256 of the raw file encoded as unpadded Base64URL>"
}size is an integer from 1 to 100,000,000 bytes. sha256 encodes the 32-byte digest, not its hexadecimal string. Accepted MIME types: video/mp4, video/webm, video/quicktime; audio/mpeg, audio/wav, audio/ogg, audio/mp4, audio/flac; image/png, image/jpeg, image/webp, image/gif. Model-specific size, duration, codec and reference limits still apply.
The HTTP 200 receipt contains url, upload_url, method: "PUT", headers (Content-Type and X-Media-Upload-Token), and expires_at (Unix seconds). This expiry is the approximately 10-minute upload permit deadline, not the media retention deadline. The URL is not ready until the upload succeeds.
Send the original binary file to upload_url with the returned method and headers. Supply the exact Content-Length and MIME; browsers sending File/Blob calculate the length automatically. Do not send multipart/form-data, JSON Base64, your API key, or an unknown-length chunked stream.
curl --request PUT "$UPLOAD_URL" \
--header 'Content-Type: video/mp4' \
--header "X-Media-Upload-Token: $UPLOAD_TOKEN" \
--data-binary @reference.mp4HTTP 201 confirms storage and returns url, size, mime_type, and expires_at. This second expiry is the media retention deadline, 30 days from object upload. Use the returned URL in the target model's reference field only after success. The upload service does not transcode, compress or extract frames. Repeated PUTs cannot overwrite the object (409); after an ambiguous upload, HEAD the permit's URL before allocating another object. Browser apps should obtain permits through their own backend, without exposing a long-lived API key.
Current public model capabilities
Veo
Veo does not accept reference video/audio. The gateway exposes metadata.video_mode: "frames" (up to two ordered images: first frame, last frame) and "reference" (up to three subject images; eight seconds required when images are present). Advanced modes require Veo 3.1 and the actual model's available capabilities; not every Fast/other variant is guaranteed to support all upstream modes. Without this field, the legacy path uses only the first image.
Explicit modes accept PNG/JPEG Base64 or image Data URLs, at most 20 MiB decoded per image. This adapter path does not automatically fetch HTTPS images into Base64. Replace the placeholder below with complete image data. Use metadata.negativePrompt, metadata.seed, and metadata.generateAudio for Veo-specific controls. Basic durations are 4/6/8 seconds and output sizes are 720P/1080P, subject to mode/model constraints.
{
"model": "veo-3.1-fast",
"prompt": "Keep the subject from the reference image and slowly push the camera forward",
"duration": 8,
"size": "1920x1080",
"images": ["data:image/jpeg;base64,<complete-image-base64>"],
"metadata": {
"video_mode": "frames",
"negativePrompt": "blur, distortion",
"generateAudio": true
}
}Grok video
Grok video accepts text or one input image. It does not accept multiple images, reference video, or reference audio. Set 480P/720P with metadata.resolution; the -1080p model is fixed at 1080P.
{
"model": "grok-imagine-video-1.5",
"prompt": "Keep the subject and slowly pull the camera back",
"duration": 8,
"size": "16:9",
"images": ["https://cdn.example.com/start.jpg"],
"metadata": { "resolution": "720p" }
}Gemini Omni video
gemini-omni-flash outputs 720P and accepts up to ten images or one reference video. Its reference video must use top-level video/videos, not metadata.referenceVideos. The video must be MP4/MOV, no longer than ten seconds, and no larger than 64 MiB.
{
"model": "gemini-omni-flash",
"prompt": "Use the motion from the video and the product from the image",
"duration": 6,
"size": "16:9",
"images": ["https://cdn.example.com/product.jpg"],
"video": "https://cdn.example.com/reference.mp4"
}| Public model | Duration | Ratios | Output | Image/video/audio limits | Keyframes | Audio control |
|---|---|---|---|---|---|---|
veo-3.1-fast | 4/6/8s, mode constraints above | 16:9, 9:16 | 720P/1080P | images by mode; 0 video/audio | model/mode dependent | native audio; metadata.generateAudio |
veo-3.1 | 4/6/8s; subject references 8s | 16:9, 9:16 | 720P/1080P | images by mode; 0 video/audio | model/mode dependent | native audio; metadata.generateAudio |
grok-imagine-video | 1–15s | 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3 | 480P/720P | 1 / 0 / 0 | no | no |
grok-imagine-video-1.5 | 1–15s | same as above | 480P/720P | 1 / 0 / 0 | no | no |
grok-imagine-video-1.5-1080p | 1–15s | same as above | 1080P | 1 / 0 / 0 | no | no |
gemini-omni-flash | 3–10s | 16:9, 9:16 | 720P | 10 / 1 / 0 | no | no |
SD2.5 720P | 4–30s | auto, 1:1, 21:9, 16:9, 9:16, 3:4, 4:3 | 720P | 30 / 10 / 10 | yes, exclusive | generate_audio |
SD2.5 480P | 4–30s | auto, 1:1, 21:9, 16:9, 9:16, 3:4, 4:3 | 480P | 30 / 10 / 10 | yes, exclusive | generate_audio |
SD2.0 720P | 4–15s | 1:1, 21:9, 16:9, 9:16, 3:4, 4:3 | 720P | 9 / 3 / 3; 12 total | yes, exclusive | not configurable |
SD2.5 1080P | 4–30s | same as SD2.5 720P | 1080P | 30 / 10 / 10 | yes, exclusive | generate_audio |
SD2.0 480P | 4–15s | same as SD2.0 720P | 480P | 9 / 3 / 3; 12 total | yes, exclusive | not configurable |
SD2.0 1080P | 4–15s | same as SD2.0 720P | 1080P | 9 / 3 / 3; 12 total | yes, exclusive | not configurable |
minimax-h3-768p | confirm current capabilities | confirm current capabilities | 768P | video support and limits must be confirmed | service dependent | not configurable |
Seedance-2.5-720p官方版 | 4–30s | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | 720P | 30 / 10 / 10 | yes, exclusive | not configurable |
Seedance-2.0-720p官方版 | 4–15s | same as above | 720P | 9 / 3 / 3 | yes, exclusive | not configurable |
Seedance-2.0-fast-720p官方版 | 4–15s | same as above | 720P | 9 / 3 / 3 | yes, exclusive | not configurable |
seedance-2.0-mini-480p | 4–15s | same as above | 480P | 9 / 3 / 3 | not published; omit | not configurable |
seedance-2.0-mini-720p | 4–15s | same as above | 720P | 9 / 3 / 3 | not published; omit | not configurable |
wan3.0-video-480p | 4–30s | 16:9, 9:16, 1:1, 4:3, 3:4 | 480P | 30 / 10 / 10 | yes | not configurable |
wan3.0-video-720p | 4–30s | 16:9, 9:16, 1:1, 4:3, 3:4 | 720P | 10 / 5 / 5 | yes, exclusive | not configurable |
wan3.0-video-1080p | 4–30s | same as above | 1080P | 10 / 5 / 5 | yes, exclusive | not configurable |
kling-3.0-turbo-720p | 4–15s | 16:9, 9:16, 1:1 | 720P | 30 / 10 / 0 | yes | generate_audio |
kling-3.0-turbo-1080p | 4–15s | same as above | 1080P | 30 / 10 / 0 | yes | generate_audio |
kling-3.0-turbo-2k | 4–15s | same as above | 2K | 30 / 10 / 0 | yes | generate_audio |
kling-3.0-turbo-4k | 4–15s | same as above | 4K | 30 / 10 / 0 | yes | generate_audio |
Available models and account access may change. Use the live model list for discovery.
Model-specific rules: Veo, Grok, and Gemini Omni use their own family examples. All three SD2.0 resolutions allow 12 ordinary references in total and have fixed generated audio. All three SD2.5 resolutions support configurable generated audio; individual reference videos are 2–30 seconds and total reference-video duration is at most 30 seconds. H3's historical 10/5/5 limits are not a guarantee across service routes: confirm video support first; supported video-reference paths require public HTTPS, and reference audio should accompany visual media. Uploading cannot add unsupported model capabilities. Wan can omit the prompt when media is supplied, but specifying it is recommended. Kling has no audio references but supports generate_audio. Keep exact public model names, including resolution suffixes.
Create response
{
"id": "task_xxxxxxxxxxxxxxxxxxxxxxxx",
"object": "video",
"model": "SD2.5 720P",
"status": "queued",
"progress": 0,
"created_at": 1788249600
}Save id. The standard interface does not guarantee task_id; new integrations must use id. HTTP 200 means the task was accepted, not that generation has finished.
Polling
curl 'https://88api.ai/v1/videos/task_xxx' \
--header 'Authorization: Bearer sk-xxxx'| Status | Meaning | Action |
|---|---|---|
queued | waiting | keep waiting |
in_progress | generating | keep waiting |
completed | finished | use the returned result url |
failed | failed | inspect error.code and error.message |
unknown | unknown | retry briefly, then contact support if persistent |
Wait 3–5 seconds before the first query and poll every 10–15 seconds. Interactive clients may stop waiting after 15 minutes, persist the task ID and resume querying later or accept a webhook. A client deadline does not cancel, fail or refund the task; per-request HTTP timeouts and server task deadlines are separate. Use status, not progress alone. Never create a duplicate simply because polling timed out.
On completion, read url (compatible aliases: video_url, result_url). Archived results use assets.88api.ai; eligible official URLs remain unchanged. Save the returned address rather than constructing a content path.
Webhooks
This section applies only to models with explicit webhook support, such as Veo. The SD, Seedance, Wan3, Kling Turbo and MiniMax H3 models listed above use polling for new requests; omit callback_url.
Set a public callback_url in the create request. It must be no longer than 2048 characters, must not target localhost or private networks, and should return a 2xx response within 15 seconds.
Terminal events are video.completed and video.failed:
{
"id": "evt_task_xxx_completed",
"type": "video.completed",
"created_at": 1788250200,
"data": {
"task_id": "task_xxx",
"model": "SD2.5 720P",
"status": "completed",
"progress": "100%",
"output_url": "https://assets.88api.ai/media/task-videos/2026/09/<object-key>.mp4",
"created_at": 1788249600,
"completed_at": 1788250200
}
}Relevant headers:
X-NewAPI-Event: video.completed
X-NewAPI-Delivery: evt_task_xxx_completed
X-NewAPI-Timestamp: 1788250200
X-NewAPI-Signature: sha256=<hex-hmac>Verify HMAC-SHA256 over:
X-NewAPI-Timestamp + "." + raw request body bytesUse the full API key, including the sk- prefix, as the HMAC secret. Compare signatures in constant time and deduplicate deliveries using X-NewAPI-Delivery or the event id.
Reject callbacks whose timestamp differs from your server time by more than five minutes to reduce replay risk.
Failed deliveries use exponential backoff. Each attempt has a 15-second timeout; delivery is attempted up to eight times, with a maximum backoff of 15 minutes. Polling may be used as a fallback alongside webhooks.
Downloading the result
After status=completed:
curl --fail --location "$RESULT_URL" --output result.mp4Use the result URL returned by polling, or data.output_url from the webhook. Follow redirects without forwarding your API key. The old /v1/videos/{id}/content path redirects archived videos but is not universal for official direct results. On a legacy 404, query the task for its actual URL instead of generating again.
Treat webhook output_url as opaque and potentially temporary. Do not parse or depend on its hostname or path layout.
Retention and cross-model reuse
Stored references and archived videos last 30 days from their respective object uploads. Reads, task queries and repeated references do not renew them. Official direct URLs have their own lifetime, not a guaranteed 30 days. Expired media may return410, or404 after deletion; an existing task record does not guarantee that its bytes still exist. Copy results to your own storage for longer retention.
Public media supports GET, HEAD, single-range requests (206), and browser CORS. X-Media-Expires-At is Unix seconds. Caching does not extend retention or guarantee identical regional download speeds. Preserve any capability parameters on URLs returned by the API; do not add query parameters to assets URLs (400), replace domains, or strip required parameters.
A Veo output can become another model's video reference if that model supports video input and the file meets its limits. Put the complete result URL directly in that model's reference field, with no download/reupload step. This does not mean Veo accepts video input. Public URLs allow anyone holding them to read the file while available.
Complete upload and polling example
Node.js 22+; set server-side API_KEY and REFERENCE_FILE (an MP4). To resume without creating a new task, set TASK_ID. The example creates one paid generation; the next request body is printed only. It buffers at most one permitted file, so bound concurrency for large files.
// video-api-example.mjs — Node.js 22+, run with API_KEY and REFERENCE_FILE.
import { readFile, stat } from "node:fs/promises";
import { createHash } from "node:crypto";
const apiKey = process.env.API_KEY;
if (!apiKey) throw new Error("Set API_KEY on your server");
const baseUrl = "https://88api.ai";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function apiJSON(path, body) {
// No automatic POST retry: a timed-out create may already have been accepted.
const response = await fetch(baseUrl + path, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(120000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
return response.json();
}
async function uploadMedia(file, mimeType) {
const info = await stat(file);
if (!info.isFile() || info.size < 1 || info.size > 100000000) {
throw new Error("File must contain 1–100000000 bytes");
}
const bytes = await readFile(file);
const permit = await apiJSON("/v1/media/uploads", {
size: bytes.length, mime_type: mimeType,
sha256: createHash("sha256").update(bytes).digest("base64url"),
});
const target = new URL(permit.upload_url);
if (target.origin !== "https://assets.88api.ai" || permit.method !== "PUT") {
throw new Error("Unexpected upload destination; do not send credentials");
}
const response = await fetch(target, {
method: "PUT", headers: permit.headers, body: bytes,
redirect: "error", signal: AbortSignal.timeout(120000),
});
// A 409/ambiguous upload is not automatically retried; HEAD permit.url first.
if (response.status !== 201) throw new Error(`Upload HTTP ${response.status}`);
return response.json();
}
async function waitForVideo(id, budgetMs = 15 * 60 * 1000) {
const deadline = Date.now() + budgetMs;
let delay = 4000;
while (Date.now() < deadline) {
await sleep(Math.min(delay, deadline - Date.now()));
const remaining = deadline - Date.now();
if (remaining <= 0) break;
let response, state;
try {
response = await fetch(`${baseUrl}/v1/videos/${encodeURIComponent(id)}`, {
headers: { Authorization: `Bearer ${apiKey}` },
signal: AbortSignal.timeout(Math.min(30000, remaining)),
});
if (response.ok) state = await response.json();
} catch {
delay = Math.min(delay * 2, 60000);
continue; // Retry a read, never create a replacement task.
}
if (response.status === 429 || response.status >= 500) {
const retryAfter = response.headers.get("Retry-After");
const seconds = retryAfter && /^\d+$/.test(retryAfter)
? Number(retryAfter)
: (Date.parse(retryAfter || "") - Date.now()) / 1000;
delay = Number.isFinite(seconds) && seconds > 0
? Math.max(1000, seconds * 1000)
: Math.min(delay * 2, 60000);
await response.body?.cancel();
continue;
}
if (!response.ok) throw new Error(`Task ${id}: HTTP ${response.status}`);
if (state.status === "completed") {
const result = state.url || state.video_url || state.result_url;
if (typeof result !== "string" || new URL(result).protocol !== "https:") {
throw new Error(`Task ${id} completed without a usable HTTPS result URL`);
}
return result; // Keep official URLs and capability query parameters intact.
}
if (state.status === "failed") {
throw new Error(`Task ${id}: ${state.error?.message || "video generation failed"}`);
}
if (!["queued", "in_progress", "unknown"].includes(state.status)) {
throw new Error(`Task ${id}: unexpected status ${state.status}`);
}
delay = 12000;
}
throw new Error(`Stopped waiting for ${id}; save this ID and query later. Not cancelled.`);
}
async function main() {
// Resume a saved task without uploading or creating another paid task.
let id = process.env.TASK_ID;
if (!id) {
if (!process.env.REFERENCE_FILE) throw new Error("Set REFERENCE_FILE");
const media = await uploadMedia(process.env.REFERENCE_FILE, "video/mp4");
const task = await apiJSON("/v1/videos", {
model: "SD2.0 720P", prompt: "Preserve the subject and motion of the reference video and continue the shot",
duration: 4, size: "16:9", metadata: { referenceVideos: [media.url] },
});
id = task.id;
if (!id) throw new Error("Create response did not contain a task ID; do not blindly retry");
console.log("Save task ID:", id); // Persist to your database in production.
}
const resultURL = await waitForVideo(id);
console.log("Result URL:", resultURL); // This grants media access; do not log publicly.
// This only shows the NEXT request body; it does not create a second paid task.
console.log(JSON.stringify({
model: "SD2.0 720P", prompt: "Preserve the subject and motion of the reference video and continue the shot",
duration: 4, size: "16:9", metadata: { referenceVideos: [resultURL] },
}, null, 2));
}
await main();Retry and error rules
- 400: invalid model, duration, ratio, field placement, or reference count.
- 401: missing or invalid API key.
- 403: the key cannot access the model or group.
- 404: task not found, not owned by the account, or content not ready.
- Upload409: object already exists; HEAD and reuse a confirmed successful upload.
- Media410: expired; references do not renew retention (404 is possible after cleanup).
- Upload415: file signature does not match its MIME type.
- Media416: invalid or out-of-bounds Range; check length with HEAD.
- 429: apply
Retry-Afteror exponential backoff. - 5xx: status queries may be retried with backoff.
status=failed: the HTTP query succeeded but generation failed; inspecterror.message.unsupported_reference_media: unsupported model capability, not fixed by uploading.must be a public HTTPS URL: upload local video first; no Blob, Data URL, HTTP or login-dependent URL.input_download_failed: check upstream reachability, DNS/network and expiry, not just playback on your computer.
POST /v1/videos does not currently guarantee customer-provided idempotency keys. If a fully transmitted create request ends with an ambiguous network timeout, do not blindly resubmit it because duplicate tasks may be generated and billed.