promptstringRequiredRequired. Describe the next beat only — the subject and the look are already carried forward from the source clip.
- Default
- Not specified
Wan 2.2 Spicy LoRA video-extend continues a source clip you supply, guided by a prompt, with the same LoRAs attached. Each call adds 5 or 8 seconds at 480p or 720p, and reusing the same weights keeps the look steady across the seam.
Examples use the selected endpoint’s public parameter definitions. Replace the API key and example media, then send the request from your server.
SDKs & integrationshttps://api.spicyapi.ai/api/v1/jobs/createTaskKeep the API key in the server environment variable SPICY_API_KEY. Use Bearer authentication and Content-Type: application/json for JSON requests.
Authorization: Bearer $SPICY_API_KEY# Generate the idempotency key once; reuse it when retrying after a lost response.
export SPICY_IDEMPOTENCY_KEY="${SPICY_IDEMPOTENCY_KEY:-$(uuidgen)}"
curl -X POST https://api.spicyapi.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $SPICY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $SPICY_IDEMPOTENCY_KEY" \
-d '{
"model": "alibaba/wan-2.2-spicy-lora/video-extend",
"input": {
"duration_seconds": 5,
"prompt": "A slow aerial shot drifting over a misty pine forest at sunrise",
"resolution": "480p",
"video_url": "https://example.com/reference.mp4"
}
}'
# Copy data.taskId from the response into TASK_ID, then poll the status.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.spicyapi.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $SPICY_API_KEY"Generated from this model’s input schema — the same one the API validates every request against.
Place these fields inside the request body’s input object. Names and types match the API. Do not assume a default when none is specified.
promptstringRequiredRequired. Describe the next beat only — the subject and the look are already carried forward from the source clip.
video_urlstringRequiredThe clip to continue. The extension starts from its final frame, so a source that ends on a settled pose joins much more cleanly. The returned video starts with the source footage and then continues it, so it runs longer than the seconds you request; trim the source part before splicing it onto the original. It has to be a direct link to the video file itself — a page that plays the video hands us HTML instead, the input check reads no duration from it, and the call fails with the charge refunded. Uploading the file is the most reliable route.
seedintegerOptional0 to 2147483647. Fix it while you try several next beats from one source clip.
resolutionstringOptionalAffects price480p or 720p, default 480p. Match the source clip unless you want the change to be visible at the seam.
"480p""720p"duration_secondsintegerOptionalAffects priceExactly 5 or 8 seconds of new material. Anything else is rejected rather than rounded.
58lorasarray<object>OptionalUp to three LoRAs, and they belong here as well as on the first pass. Repeat the same URLs and strengths, or the style shifts at the join. Each path can be at most 500 characters in every LoRA list on this endpoint.
loras[].pathstringRequired within objectDirect download URL of the LoRA weights file.
loras[].scalenumberOptionalStrength of this LoRA. Defaults to 1.
high_noise_lorasarray<object>OptionalUp to three LoRAs applied only in the high-noise stage, where composition and motion are decided. The right place for a camera or movement style.
high_noise_loras[].pathstringRequired within objectDirect download URL of the LoRA weights file.
high_noise_loras[].scalenumberOptionalStrength of this LoRA. Defaults to 1.
low_noise_lorasarray<object>OptionalUp to three LoRAs applied only in the low-noise stage, where detail and texture resolve. The right place for line art, grain or a rendering treatment.
low_noise_loras[].pathstringRequired within objectDirect download URL of the LoRA weights file.
low_noise_loras[].scalenumberOptionalStrength of this LoRA. Defaults to 1.
{
"additionalProperties": false,
"properties": {
"duration_seconds": {
"default": 5,
"description": "Requested output length in seconds.",
"enum": [
5,
8
],
"type": "integer",
"x-ui": {
"advanced": false,
"affects_price": true,
"order": 980,
"unit": "s",
"widget": "radio"
}
},
"high_noise_loras": {
"description": "LoRAs applied only in the high-noise stage, which sets composition and motion. Up to three.",
"items": {
"additionalProperties": false,
"properties": {
"path": {
"contentMediaType": [
"application/octet-stream"
],
"description": "Direct download URL of the LoRA weights file.",
"format": "uri",
"maxLength": 500,
"minLength": 1,
"type": "string"
},
"scale": {
"default": 1,
"description": "Strength of this LoRA. Defaults to 1.",
"maximum": 4,
"minimum": 0,
"type": "number"
}
},
"required": [
"path"
],
"type": "object"
},
"maxItems": 3,
"type": "array",
"x-ui": {
"order": 1005,
"widget": "object-list"
}
},
"loras": {
"description": "LoRAs applied to the whole generation, up to three.",
"items": {
"additionalProperties": false,
"properties": {
"path": {
"contentMediaType": [
"application/octet-stream"
],
"description": "Direct download URL of the LoRA weights file.",
"format": "uri",
"maxLength": 500,
"minLength": 1,
"type": "string"
},
"scale": {
"default": 1,
"description": "Strength of this LoRA. Defaults to 1.",
"maximum": 4,
"minimum": 0,
"type": "number"
}
},
"required": [
"path"
],
"type": "object"
},
"maxItems": 3,
"type": "array",
"x-ui": {
"affects_price": false,
"order": 985,
"widget": "object-list"
}
},
"low_noise_loras": {
"description": "LoRAs applied only in the low-noise stage, which refines detail and texture. Up to three.",
"items": {
"additionalProperties": false,
"properties": {
"path": {
"contentMediaType": [
"application/octet-stream"
],
"description": "Direct download URL of the LoRA weights file.",
"format": "uri",
"maxLength": 500,
"minLength": 1,
"type": "string"
},
"scale": {
"default": 1,
"description": "Strength of this LoRA. Defaults to 1.",
"maximum": 4,
"minimum": 0,
"type": "number"
}
},
"required": [
"path"
],
"type": "object"
},
"maxItems": 3,
"type": "array",
"x-ui": {
"order": 1010,
"widget": "object-list"
}
},
"prompt": {
"description": "Describe the action, the camera, and the mood of the shot.",
"type": "string",
"x-ui": {
"affects_price": false,
"order": 990,
"widget": "textarea"
}
},
"resolution": {
"default": "480p",
"description": "Output resolution.",
"enum": [
"480p",
"720p"
],
"type": "string",
"x-ui": {
"advanced": false,
"affects_price": true,
"order": 970,
"widget": "radio"
}
},
"seed": {
"description": "Random seed. Keep it fixed only for controlled comparisons, not as a frame-identical guarantee.",
"maximum": 2147483647,
"minimum": 0,
"type": "integer",
"x-ui": {
"affects_price": false,
"order": 960,
"widget": "number"
}
},
"video_url": {
"contentMediaType": [
"video/mp4",
"video/webm",
"video/quicktime"
],
"description": "Source clip to continue. The new footage follows its final frame.",
"format": "uri",
"minLength": 1,
"type": "string",
"x-ui": {
"affects_price": false,
"order": 1000,
"widget": "upload"
}
}
},
"required": [
"video_url",
"prompt"
],
"type": "object",
"x-pricing": {
"billingMode": "variant_matrix",
"variantFields": [
"resolution"
]
}
}HTTP 202 means the task was accepted, not completed. Save the returned taskId and query that task. Do not create another task while waiting for its result.
HTTP/1.1 202 Accepted
Location: /api/v1/jobs/recordInfo?taskId=job_01k3m8x9q2z4v7n5p6r8s0t1w3
{
"code": 200,
"msg": "success",
"request_id": "req_example_create",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued",
"estimatedCost": "0.750000"
}
}The accepted response includes the estimate so your app can show the same amount in task history and billing.
curl --get "https://api.spicyapi.ai/api/v1/jobs/recordInfo" \
--data-urlencode "taskId=$TASK_ID" \
-H "Authorization: Bearer $SPICY_API_KEY"Start polling at about 2 seconds and gradually increase the interval. Stop at a terminal state. After a connection failure, query the original taskId. Retry creation only with the original idempotency key and body.
import { verifyWebhook } from "@spicyapi/sdk";
// express.raw keeps the bytes we signed; express.json never matches.
app.post("/hooks/spicy", express.raw({ type: "application/json" }), (req, res) => {
const { payload } = verifyWebhook({
rawBody: req.body,
timestamp: req.header("X-Webhook-Timestamp"),
signature: req.header("X-Webhook-Signature"),
payloadVersion: Number(req.header("X-Webhook-Payload-Version")),
secret: process.env.SPICY_WEBHOOK_SECRET,
});
res.status(200).end();
void handleFinishedTask(payload);
});Pass callBackUrl when you create the task and the finished record is POSTed to you — no polling loop at all. Every delivery carries X-Webhook-Timestamp, X-Webhook-Signature and X-Webhook-Payload-Version. The signature is a base64 HMAC-SHA256 over taskId.timestamp.sha256(body), so it must be checked against the raw bytes: parsing the JSON and re-serialising it changes them and the check will fail. Failed deliveries are retried, so the same taskId can arrive twice — make the handler idempotent. Answer 2xx first and do the work afterwards; a slow handler is treated as a failed delivery.
Consume output only when state is succeeded. On failure, use errorCode, errorMessage and request_id to investigate. Example costs and states do not represent an actual bill.
{
"code": 200,
"msg": "success",
"request_id": "req_example_record",
"data": {
"taskId": "job_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "alibaba/wan-2.2-spicy-lora/video-extend",
"state": "succeeded",
"output": {
"assets": [
{
"key": "tasks/2026/08/28/job_01k3m8x9q2z4v7n5p6r8s0t1w3/3f7a1c9e5b2d48f0a6c1e3d5b7f9a1c3.mp4",
"mime": "video/mp4",
"url": "https://example.r2.cloudflarestorage.com/results/job_01k3m8x9q2z4v7n5p6r8s0t1w3/result.mp4?X-Amz-Signature=SIGNATURE_FROM_RESPONSE",
"expiresAt": "2026-08-28T12:20:08.000Z"
}
]
},
"cost": "0.750000",
"settled": true,
"createdAt": "2026-08-28T12:00:00Z",
"completedAt": "2026-08-28T12:00:30Z"
}
}For each available asset, open or download assets[].url directly without an API Authorization header. The signed URL expires at expiresAt; poll the task again to refresh it while the asset remains within its retention period. A pending or unavailable asset has no URL. Treat signed URLs as private.
Use SpicyAPI in your app, terminal, AI assistant or visual workflow. Choose a setup guide below.
You do not have to write any of the code above. Connect the MCP server once and any agent that speaks MCP — Claude Code, Codex, Cursor, Windsurf, VS Code, Gemini CLI, Cline, LibreChat — can run this model by name, or run it from your shell with a single command.
Use alibaba/wan-2.2-spicy-lora/video-extend to make a five-second shot of a neon-lit rooftop at dusk, slow push-in, and save it to ./out/.
The agent looks the model up in the live catalogue, uploads the local file for you, then shows the exact quote and its maximum charge and waits for your confirmation. Nothing is billed until you say yes.
claude mcp add spicyapi -e SPICY_API_KEY=$SPICY_API_KEY \
-- npx --yes --package=@spicyapi/mcp spicyapi-mcp
npx skills add spicyapi-ai/spicy-skillThe first line gives the agent the tools; the second gives it the Skill — the operating rules for them, so it quotes before it bills, uploads local files instead of pasting bytes, and reads the finished asset URL rather than guessing. Every other client takes the same server through its own config file: see the developer hub.
npx @spicyapi/cli tasks create --model alibaba/wan-2.2-spicy-lora/video-extend \
--input-json '{"duration_seconds":5,"prompt":"A slow aerial shot drifting over a misty pine forest at sunrise","resolution":"480p","video_url":"https://example.com/reference.mp4"}' \
--waitThe input is this endpoint’s own schema, same as the examples above. --wait polls until the task is finished and prints it; add --yes to skip the confirmation prompt in CI. Set SPICY_API_KEY first — one key works for the API, the CLI and the MCP server.
npx @spicyapi/cli files upload ./poster.png
# -> spicy://uploads/01k3m8x9q2z4v7n5p6r8s0t1w3
npx @spicyapi/cli tasks create --model alibaba/wan-2.2-spicy-lora/video-extend \
--input-json '{"video_url":"spicy://uploads/01k3m8x9q2z4v7n5p6r8s0t1w3"}' \
--waitRequest bodies take URLs, not bytes. Upload the file first and pass the spicy:// URI it prints — the field name above is this endpoint’s own, straight from its schema. An agent on MCP does this step for you, which is why the sentence further up can name ./poster.png directly. Media already reachable over public HTTPS needs no upload.
npx @spicyapi/cli tasks create --model alibaba/wan-2.2-spicy-lora/video-extend \
--input-json '{…}' --retention 1h --wait
npx @spicyapi/cli tasks get $TASK_ID # read back the retention that applied--retention only ever shortens the window — 30m, 1h, 7d, or 0 to delete as soon as the task reaches a terminal state. The value that actually applies is the shortest of your request, your account settings and the platform maximum, so read it back from the task record rather than assuming the number you asked for took effect. Passing a longer window than your account allows does nothing.
Install the SpicyAPI nodes from ComfyUI Manager or with the command below, then search for Wan 2.2 Spicy LoRA · Video Extend on the canvas. The node has the same options as this page and shows its price before it runs.
cd ComfyUI/custom_nodes
git clone https://github.com/spicyapi-ai/comfyui-spicyapiThe embedded panel reads the same schema as this page, so it cannot drift from the API. If your docs need the content to be searchable, read the JSON under /api/embed/models/ and render it yourself.
<iframe src="https://spicyapi.ai/embed/en/models/wan-2-2-spicy-lora/api?model=alibaba%2Fwan-2.2-spicy-lora%2Fvideo-extend" width="100%" height="560" loading="lazy" title="Request example" style="border:1px solid rgba(0,0,0,.12)"></iframe>