Tasks
Every call to POST /v1/generate creates a task. GET /v1/tasks/{id} returns its current state and, once it has finished, the output files and the final cost.
Request
curl https://jantaris.com/v1/tasks/task_8f2k... \
-H "Authorization: Bearer $JANTARIS_API_KEY"The poll_url field returned by /v1/generate already contains the full URL.
Response
{
"id": "task_8f2k...",
"status": "succeeded",
"model": "seedance-2.5",
"output": [
{ "type": "video", "url": "https://...", "duration": 5, "width": 1280, "height": 720 }
],
"cost": { "amount": "0.77", "currency": "USD" },
"error": null,
"created_at": "2026-09-27T10:15:02Z",
"completed_at": "2026-09-27T10:16:41Z"
}| Field | Type | Description |
|---|---|---|
id | string | Task id |
status | string | queued, running, succeeded or failed |
model | string | Model id |
output | array | Result files, filled when status is succeeded |
cost | object | {amount, currency} charged for the task. 0 when failed |
error | object or null | {type, message} when status is failed |
created_at | string | ISO 8601 timestamp |
completed_at | string or null | ISO 8601 timestamp, set when the task finishes |
Statuses
| Status | Meaning | Final |
|---|---|---|
queued | Accepted, waiting to start. Funds are held | no |
running | Generation in progress | no |
succeeded | Output ready, cost charged | yes |
failed | Generation did not complete, hold released, you pay 0 | yes |
Output items
| Field | Present for | Description |
|---|---|---|
type | all | image, video or audio |
url | all | Temporary download URL |
width, height | image, video | Pixels |
duration | video, audio | Seconds |
A failed task:
{
"id": "task_3xq1...",
"status": "failed",
"model": "kling-3.0",
"output": [],
"cost": { "amount": "0.00", "currency": "USD" },
"error": { "type": "upstream_error", "message": "The model could not complete this request." },
"created_at": "2026-09-27T10:20:00Z",
"completed_at": "2026-09-27T10:21:12Z"
}Polling
Poll every few seconds until the status is final. Images usually need seconds, video can take several minutes.
import os, time, requests
API = "https://jantaris.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['JANTARIS_API_KEY']}"}
def wait_for_task(task_id: str, timeout: int = 900) -> dict:
delay, deadline = 2, time.time() + timeout
while time.time() < deadline:
task = requests.get(f"{API}/tasks/{task_id}", headers=HEADERS, timeout=30).json()
if task["status"] in ("succeeded", "failed"):
return task
time.sleep(delay)
delay = min(delay * 1.5, 15) # back off up to 15 s
raise TimeoutError(f"task {task_id} still running after {timeout} s")
task = requests.post(
f"{API}/generate",
headers=HEADERS,
json={"model": "kling-3.0", "input": {"prompt": "A paper boat drifting down a rainy street", "duration": 5}},
timeout=30,
).json()
done = wait_for_task(task["id"])
print(done["status"], done["cost"], done["output"] or done["error"])const API = "https://jantaris.com/v1";
const headers = { Authorization: `Bearer ${process.env.JANTARIS_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function waitForTask(id, timeoutMs = 900_000) {
let delay = 2000;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const task = await (await fetch(`${API}/tasks/${id}`, { headers })).json();
if (task.status === "succeeded" || task.status === "failed") return task;
await sleep(delay);
delay = Math.min(delay * 1.5, 15000);
}
throw new Error(`task ${id} still running`);
}
const started = await (await fetch(`${API}/generate`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ model: "kling-3.0", input: { prompt: "A paper boat drifting down a rainy street", duration: 5 } }),
})).json();
const done = await waitForTask(started.id);
console.log(done.status, done.cost, done.output ?? done.error);For long jobs or many parallel tasks, webhooks save you the polling loop.
Downloading output
Output URLs are temporary. Download the files you want to keep and store them on your side.
import pathlib
def download_outputs(task: dict, folder: str = "out") -> list[pathlib.Path]:
ext = {"image": "png", "video": "mp4", "audio": "mp3"}
paths = []
pathlib.Path(folder).mkdir(exist_ok=True)
for i, item in enumerate(task["output"]):
path = pathlib.Path(folder) / f"{task['id']}_{i}.{ext[item['type']]}"
with requests.get(item["url"], stream=True, timeout=120) as r:
r.raise_for_status()
with open(path, "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
paths.append(path)
return pathsimport { writeFile, mkdir } from "node:fs/promises";
async function downloadOutputs(task, folder = "out") {
await mkdir(folder, { recursive: true });
for (const [i, item] of task.output.entries()) {
const res = await fetch(item.url);
const buf = Buffer.from(await res.arrayBuffer());
await writeFile(`${folder}/${task.id}_${i}`, buf);
}
}The file extension above is a guess by type. Use the Content-Type header of the download if you need the exact format.
Errors
| Status | error.type | When |
|---|---|---|
| 401 | authentication_error | Bad or missing key |
| 404 | not_found | Task id does not exist or belongs to another account |
See Errors.









