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-10-10. Model-specific parameters and examples now live on separate brand pages; see Brand pages. 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, official Seedance and Mini, Seedance Official Relay, Wan3, 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://api.88api.ai/v1/videos |
| Retrieve task status | GET https://api.88api.ai/v1/videos/{id} |
| Request an upload permit | POST https://api.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://api.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 api.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://api.88api.ai/v1/videos' \
--header 'Authorization: Bearer sk-xxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
"model": "SD2.5",
"resolution": "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 |
Some models fix the output resolution in the public model name; resolution cannot override those. Others (for example SD, official Seedance, Kling, Google, MiniMax, Seedance Official Relay, Wan and Grok) select it with resolution. Field names also differ by brand; follow the brand pages.
Reference media
Reference images
Use the top-level images array for consistent behavior across current models:
{
"model": "SD2.5",
"resolution": "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 and Kling families that support multiple references, put video and audio references in metadata (Wan recommends the media array; see Wan Video API):
{
"model": "SD2.5",
"resolution": "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.
For MiniMax H3 reference images, do not use top-level images; use metadata.referenceImages as described in MiniMax Video API.
First and last frames
{
"model": "seedance-2.5官方版",
"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 in Google Video API.
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
Brand pages
Parameter names and media fields differ between brands. Read the brand page before integrating:
| Brand page | Models |
|---|---|
| Seedance Official Edition | seedance-2.5官方版, seedance-2.0官方版, seedance-2.0-fast官方版, seedance-2.0-mini官方版 |
| SD Cloud Edition | SD2.5, SD2.0 |
| Seedance Official Relay | Seedance 2.0 Mini (multi-resolution), Seedance 2.0 (multi-resolution), Seedance 2.0 Fast (multi-resolution), Seedance 2.5 (multi-resolution) |
veo-3.1, veo-3.1-fast, gemini-omni-flash, gemini-omni-flash-1.1 | |
| Kling | kling-3.0-turbo |
| MiniMax | minimax-h3 (same as minimax_h3) |
| Wan | wan3.0-video, wan3.0-video-prime |
| Grok | grok-imagine-video, grok-imagine-video-1.5 |
Capability overview
| Public model | Duration | Ratios | Output | Image/video/audio limits | Keyframes | Audio control |
|---|---|---|---|---|---|---|
seedance-2.5官方版 | 4–30s, default 4s | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | 720P | 30 / 10 / 10 | yes, exclusive | not configurable |
seedance-2.0官方版 | 4–15s, default 4s | same as above | 720P | 9 / 3 / 3 | yes, exclusive | not configurable |
seedance-2.0-fast官方版 | 4–15s, default 4s | same as above | 720P | 9 / 3 / 3 | yes, exclusive | not configurable |
seedance-2.0-mini官方版 | 4–15s, default 5s | same as above | 480P/720P, default 480P | 9 / 3 / 3 | not published; omit | not configurable |
SD2.5 | 4–30s | required: 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | 480P/720P/1080P, default 480P | 30 / 10 / 10 | yes, exclusive | generate_audio |
SD2.0 | 4–15s | 1:1 (default), 21:9, 16:9, 9:16, 3:4, 4:3 | 480P/720P/1080P/4K, default 480P | 9 / 3 / 3; 12 total | yes, exclusive | not configurable |
| Seedance 2.0 Mini (multi-resolution) | 4–15s, default 5s; -1 allowed | adaptive (default), 16:9, 4:3, 1:1, 3:4, 9:16, 21:9 | 480P/720P | 9 / 3 / 3 | yes, exclusive | audio on by default; can be turned off |
| Seedance 2.0 (multi-resolution) | 4–15s, default 5s; -1 allowed | same as above | 480P/720P/1080P/4K | 9 / 3 / 3 | yes, exclusive | audio on by default; can be turned off |
| Seedance 2.0 Fast (multi-resolution) | 4–15s, default 5s; -1 allowed | same as above | 480P/720P | 9 / 3 / 3 | yes, exclusive | audio on by default; can be turned off |
| Seedance 2.5 (multi-resolution) | 4–30s, default -1 (automatic) | same as above | 480P/720P/1080P | 30 / 10 / 10 | yes, exclusive | audio on by default; can be turned off |
veo-3.1-fast | 4/6/8s; reference images 8s; extension adds 7s | 16:9, 9:16 | 720P/1080P/4K | 3 / 0 / 0 | first frame, first and last frames, reference images | native audio; generate_audio can turn it off |
veo-3.1 | 4/6/8s; reference images 8s; extension adds 7s | 16:9, 9:16 | 720P/1080P/4K | 3 / 0 / 0 | first frame, first and last frames, reference images | native audio; generate_audio can turn it off |
gemini-omni-flash | 3–10s | 16:9, 9:16 | 720P | 10 / 1 / 0 | first frame, first and last frames | no |
gemini-omni-flash-1.1 | 3–10s; extension adds about 10s | 16:9, 9:16 | 720P/1080P/4K | 10 / 3 / 0 | first frame, first and last frames | no |
kling-3.0-turbo | 4–15s | 16:9, 9:16, 1:1 | 720P/1080P/2K/4K, default 720P | 30 (including keyframes) / 10 / 0 | yes, can be combined with reference images | generate_audio |
minimax-h3 | 4–15s, default 5s | 16:9 (default), 9:16, 1:1, 4:3, 3:4, 21:9 | 768P/1080P/2K (upscaled), default 768P | 9 / – / 3 | yes, exclusive | always includes audio; not configurable |
wan3.0-video | 2–30s, default 5s; -1 allowed | adaptive (default), 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | 480P/720P/1080P, default 1080P | 10 / 5 / 5 (in the media array) | yes, exclusive | not configurable |
wan3.0-video-prime | same as above | same as above | same as above | same as above | same as above | not configurable |
grok-imagine-video | 1–15s, default 8s | 16:9, 9:16, 1:1, 4:3, 3:4, 3:2, 2:3 (via aspect_ratio) | 480P/720P | 1 / 0 / 0 | no | no |
grok-imagine-video-1.5 | 1–15s, default 8s | same as above | 480P/720P/1080P | 1 / 0 / 0 | no | no |
Available models and account access may change. Use the live model list for discovery.
Model-specific rules that are easy to get wrong (see the brand pages above for the full rules):
- Wan
wan3.0-video*: send first frames, last frames and references in themediaarray (recommended).metadata.firstFrame/lastFrame/referenceImages/referenceVideos/referenceAudios(snake_case forms too), a top-levelfirstFrame/first_frame/lastFrame/last_frameand top-levelimages(1st = first frame, 2nd = last frame, at most 2) are also converted tomedia. Material fields that cannot be read return 400. - Grok treats
aspect_ratioandratioas equivalent; if you send several ratio fields, their values must match. At most one input image. minimax-h3takes reference images inmetadata.referenceImagesand first/last frames inmetadata.firstFrame/lastFrame; the meaning of top-levelimagesvaries with the assigned route, so do not rely on it. 480P and 4K have been retired.- Veo and Gemini Omni take reference videos in top-level
video/videos, notmetadata.referenceVideos. - Seedance Official Relay also accepts the singular
image,input_reference,videoandaudiofields as reference media, and the same media sent twice counts once; asizethat conflicts withratio,aspect_ratioorresolutionreturns 400. The group also supports the virtual portrait library, 2.5 drafts, last frames, web search, cancelling queued tasks and official task IDs; see Seedance Official Relay. SD2.5requires an explicit ratio and does not supportauto.- Official Seedance,
SD2.5,SD2.0andkling-3.0-turboeach sell one model name;resolutionselects and bills the tier. Without it the lowest tier is used, and an unsupported tier returns 400. The earlier names with a tier suffix (such asSD2.5 720PorSD2.0 4K) still work at the fixed tier in their name, but are no longer listed in the model square. The Kling names with a tier suffix have been retired. Uploading cannot add unsupported model capabilities.
Create response
{
"id": "task_xxxxxxxxxxxxxxxxxxxxxxxx",
"object": "video",
"model": "SD2.5",
"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://api.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. Google video models (Veo, Gemini Omni) and the SD, official Seedance, Seedance Official Relay, 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",
"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. Veo itself accepts an input video only for extension, not as a reference. 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, MODEL_ID, 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://api.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.MODEL_ID) throw new Error("Set MODEL_ID from GET /v1/models");
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: process.env.MODEL_ID, prompt: "Preserve the subject and motion of the reference video and continue the shot",
duration: 4, ratio: "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: process.env.MODEL_ID, prompt: "Preserve the subject and motion of the reference video and continue the shot",
duration: 4, ratio: "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.