You have called createTask and got a taskId back instead of an image. Now you need the result without holding a connection open for minutes, paying twice after a timeout, or losing the job when your process restarts.
The short version:
- Quick image models in a script: add
?wait=30tocreateTaskand often skip polling entirely. - Scripts, local development, no public server: poll the task with a bounded loop like the one below.
- Anything a user is waiting on, video, or volume: use
callBackUrl, verify the signature, acknowledge fast, and process from a queue. - Production: do both. The callback is the fast path; a periodic status sweep catches the callbacks that never arrive.
In the raw API, ?wait=<seconds> (up to 60) holds createTask open until the task ends, which suits image models that finish in seconds and not video. callBackUrl (capital B) is the public HTTPS address that receives the result, and recordInfo is the status call that the SDK's get_task wraps. The task reference has all three.
Try it in the Playground first
Open the seedream-5-0-pro Playground. Type a short Prompt, set Resolution to 1k, press Run model, and read the result in Output; History lists earlier runs. The wait you see is what polling and webhooks automate for your application; they do not create a second image.
Choose the completion path
| Your application | Start with | Why |
|---|---|---|
| Local script or occasional batch | Polling through the SDK | No public receiver to deploy |
| Interactive image or video application | Verified webhook | React when generation finishes without keeping one waiting process per job |
| Production service | Webhook plus reconciliation sweep | A missed callback does not leave an accepted task orphaned |
Choose polling when the job count is small or your machine cannot receive public HTTPS calls. Choose webhooks when users are waiting on a live application or long video jobs would tie up workers.
For image-input tasks, the SDK upload flow performs /files/{fileId}/commit and returns a spicy:// URI; see the media documentation.
Run one task with the SDK
Install spicyapi, set SPICY_API_KEY, and run the example. Keep map-image.json to resume the same task after interruption. For a deliberately different image, use a new run name. The Python SDK supplies the API methods; the task reference covers the complete lifecycle.
import json
import os
import time
import uuid
from pathlib import Path
from spicyapi import SpicyClient, output_assets
client = SpicyClient()
def run(model, inputs, name):
path = Path(name + ".json")
request = {"model": model, "input": inputs}
saved = json.loads(path.read_text()) if path.exists() else {"request": request, "key": str(uuid.uuid4())}
if saved["request"] != request:
raise ValueError("Changed input needs a new run name; preserve the old attempt.")
def save():
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(saved))
temporary.chmod(0o600)
temporary.replace(path)
if not saved.get("taskId"):
if time.time() - saved.get("sentAt", time.time()) >= 23 * 3600:
raise RuntimeError("Reconcile the saved attempt before submitting again.")
if "quote" not in saved:
saved["quote"] = client.quote_task(model=model, input_data=inputs)
if input(f"Maximum ${saved['quote']['maxCharge']}. Create? [y/N] ") != "y":
raise SystemExit("Stopped without creating this task.")
saved.setdefault("sentAt", time.time())
save()
quote = saved["quote"]
task = client.create_task(model=model, input_data=inputs,
idempotency_key=saved["key"], quote_id=quote["quoteId"], expected_cost=quote["estimatedCost"])
saved["taskId"] = task["taskId"]
save()
print("Task:", saved["taskId"])
for _ in range(360):
task = client.get_task(saved["taskId"])
if task["state"] not in {"queued", "running"} and task["state"] != "succeeded":
raise RuntimeError(str(task.get("errorMessage", task["state"])))
if task["state"] == "succeeded":
assets = output_assets(task)
if assets and assets[0].get("unavailable"):
raise RuntimeError("Output unavailable; keep the task ID for reconciliation.")
if assets and assets[0].get("url") and not assets[0].get("pending"):
return assets[0]
time.sleep(5)
raise TimeoutError("Rerun the same saved attempt to resume; no new task is needed.")
asset = run("bytedance/seedream-5.0-pro/text-to-image", {
"prompt": "a folded paper map on a wooden table, overhead", "resolution": "1k", "output_format": "png"
}, "map-image")
print(asset["url"])Read the response correctly
A task is always in one of these states: queued, running, succeeded, failed or expired. Only the first two are still moving, so end your loop on "not queued or running" rather than waiting for succeeded, or a failed task polls forever (older records may also show a read-only canceled). The result link sits at output.assets[0].url. When you call the raw API, read the task from the envelope's data object, not the response root. A task you have accepted cannot be canceled, and only succeeded is charged; failed and expired normally release the held price.
What a timeout means for your application
HTTP 202 means a task was accepted, not that generation finished. A local timeout stops waiting; it does not cancel the accepted task. Keep its task ID and resume it rather than buying another generation. A task's returned deadline is the recovery boundary, not a prediction of the render time.
- Submission response lost: retain the request and its key so recovery can find the original task.
- Generation failed: surface the model's error and let the user deliberately choose another attempt.
- Generation succeeded, file pending: keep waiting on that task; the image or video is still being prepared for delivery.
A new request gets a new idempotency key. Recovery of the same request keeps its key and input unchanged. The idempotency reference describes the 24-hour replay window.
Use webhooks for the fast path
Send completion events to your application backend, then update the user interface through your usual notification path. A callback is not a direct message to the user's browser.
The receiver should do four things:
- Verify the event using the official SDK and the raw request body.
- Confirm that the task belongs to a job your application owns.
- Persist and deduplicate the event before acknowledging it.
- Queue output copying and the application update separately from the HTTP response.
Choose it when: you already run a backend and want prompt completion updates. Look elsewhere when: this is a short local script with no public receiver; polling is simpler.
Treat task completion and media delivery as separate checks. A well-formed event can describe a failed task, while a succeeded task may need file handling before your application says it is ready. The webhook documentation contains the verification and delivery contract.
Keep a reconciliation sweep
Save every accepted task ID. Periodically revisit tasks your application has not resolved, especially after a deploy, network outage or signing-secret rotation. Feed a recovered result through the same idempotent completion handler as a webhook, so the gallery does not receive the same file twice.
Keep unresolved billing in the journal until settlement finishes. If local records are lost, current-key task history can recover the IDs; production integration covers this path.
Copy results before they expire
Ready output links are temporary. Copy files to storage you control, with a User-Agent request header and a filename matching the returned format. A download failure is a reason to recover the existing result, not regenerate it.
Use media retention and delivery for the deadlines and durable storage for the copy workflow.
The decision
Start with SDK polling for a small script. Add verified webhooks when you have an interactive service, and keep a sweep for recovery. The saved task ID connects all three paths: they collect one generation rather than submit replacements.
For more simultaneous work, add bounded batch dispatch. For the first image-to-video pair, use the starter guide.
Let an Agent handle the call
Use an Agent when you want it to prepare the inputs and return the result while you approve the price and creative choices.
- CLI: save
{"prompt": "a folded paper map on a wooden table, overhead", "resolution": "1k", "output_format": "png"}asinput.json, then usenpx @spicyapi/cli tasks create --model bytedance/seedream-5.0-pro/text-to-image --input-file input.json --wait. The CLI confirms the price; keep the returned task ID for recovery. - MCP: ask your assistant to use
spicyapi_task_createforbytedance/seedream-5.0-pro/text-to-image, thenspicyapi_task_wait. The MCP server requests price confirmation before creation. - Skill: install with
npx skills add spicyapi-ai/spicy-skill, then ask$spicyapito run the same workflow and stop for your approval. The Skill uses a configured CLI or MCP connection.







