Submitting jobs
The render and simulation job lifecycle — job types, cost estimation, the upload→submit flow, idempotency, and polling.
Type: How-to · Audience: Pipeline TDs, researchers, automation builders
The job lifecycle
estimate (optional) → upload-url → PUT file → finalize → submit → poll status → outputs
All five steps use the same /v1/jobs/* namespace. Estimate and upload-url are
optional but recommended: estimate before spending credits, upload before
referencing a scene file.
Large inputs (films, VFX caches, model weights)? The single-PUT
upload-urlis capped at 5 GiB and is not resumable. Use the resumable, integrity-checked multipart flow (POST /v1/uploads) instead, then submit the returnedkeyassceneFile. There is no 25 MB-class limit — bytes go straight to object storage. See Large-file uploads.
Job status values may include SUBMITTING, SUBMITTED, PENDING, RUNNING, PAUSED, MIGRATING, RESTORING, SUCCEEDED, COMPLETED, FAILED, CANCELLED, CANCELED, and UNKNOWN. Treat SUCCEEDED/COMPLETED, FAILED, and CANCELLED/CANCELED as terminal. JSON polling and SSE status events may include sanitized taskFailures entries with only taskId, code, exitCode, and retryCount.
For the hosted, default-off checkpoint-and-restore experiment, check StateBridge preview compatibility before estimating or uploading. The guide documents the canonical recovery object, CPU and CUDA requirements, controls, and evidence limits.
Job types
The jobType field is the discriminator that routes the job to the right backend
and pricing model.
Render jobs (jobType: "render")
Frame-based DCC render jobs. Cloud rendering is in early access for Blender; other hosts fail with an explicit availability error until their render lanes open (see DCC Plugins).
Required fields: sceneFile, frameStart, frameEnd
{
"jobType": "render",
"sceneFile": "uploads/scene-abc123.blend",
"frameStart": 1,
"frameEnd": 250,
"outputFormat": "EXR",
"jobName": "product-shot-v3"
}
Simulation jobs (jobType: "simulation")
Task-based jobs for embarrassingly parallel scientific workloads, such as coastal wave simulation (SWAN / WAVEWATCH III). Tightly coupled MPI workloads (GROMACS, OpenFOAM, WRF) are not yet supported.
Stateful VFX sims (Blender/Houdini physics bakes) use a different, sequential
request shape (frameStart/frameEnd instead of taskCount) — see
Simulations for that contract, its cap-as-hold billing
model, and the gated-preview status.
Required fields: sceneFile (the input dataset), simulator, taskCount.
{
"jobType": "simulation",
"sceneFile": "uploads/swan-cases.tar",
"simulator": "swan",
"taskCount": 24,
"jobName": "offshore-wind-swan-ensemble"
}
Estimate with the same simulation inputs, review the quoted cost as the
confirmation checkpoint, and submit only after approval. A successful job's
outputs include downloadable result artifacts.
Cost estimation
Always estimate before submitting expensive jobs.
curl -X POST https://kinocloud.io/api/v1/jobs/estimate \
-H "Authorization: Bearer kc_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobType": "render",
"frameStart": 1,
"frameEnd": 1000,
"metadata": {
"software": "blender",
"renderer": "cycles",
"totalSizeGB": 2.5
}
}'
The breakdown field shows per-component costs so you can tune the job
before committing.
The upload → submit flow
Scene files and input datasets must be uploaded to KinoCloud storage first. The presigned URL flow keeps large files off your API server.
import hashlib, requests, os
API = "https://kinocloud.io/api/v1"
KEY = os.environ["KINOCLOUD_API_KEY"]
# 1. Hash and presign
with open("scene.blend", "rb") as f:
sha256 = hashlib.file_digest(f, "sha256").hexdigest()
r = requests.post(f"{API}/jobs/upload-url",
headers={"Authorization": f"Bearer {KEY}"},
json={"filename": "scene.blend",
"contentType": "application/x-blend",
"size": os.path.getsize("scene.blend"),
"sha256": sha256})
r.raise_for_status()
upload = r.json()
# 2. Upload directly to cloud storage (no auth header)
with open("scene.blend", "rb") as f:
requests.put(upload["uploadUrl"],
headers={"Content-Type": "application/x-blend"},
data=f).raise_for_status()
# 3. Finalize; only a verified key is eligible for submission
verified = requests.post(f"{API}/jobs/upload-url/finalize",
headers={"Authorization": f"Bearer {KEY}"},
json={"finalizeToken": upload["finalizeToken"]})
verified.raise_for_status()
# 4. Submit using the verified key
r = requests.post(f"{API}/jobs/submit",
headers={"Authorization": f"Bearer {KEY}"},
json={"jobType": "render",
"sceneFile": verified.json()["key"],
"frameStart": 1, "frameEnd": 250,
"outputFormat": "EXR"})
r.raise_for_status()
job = r.json()
print(job["jobId"]) # job_01JABCDE12345
Idempotent submission
Add an Idempotency-Key header to make retries safe. If KinoCloud has already
processed a request with that key, it replays the original response instead of
submitting a duplicate job.
curl -X POST https://kinocloud.io/api/v1/jobs/submit \
-H "Authorization: Bearer kc_live_YOUR_KEY" \
-H "Idempotency-Key: render-job-2026-06-08-scene-v3" \
-H "Content-Type: application/json" \
-d '{ "jobType": "render", "sceneFile": "...", "frameStart": 1, "frameEnd": 250 }'
Idempotency keys are scoped per user. Use a key that uniquely identifies the logical job — a hash of the scene file + frame range works well.
The response includes "idempotentReplay": true when a matching request is replayed. Reusing the key with a different payload returns 409 IDEMPOTENCY_CONFLICT. Submitting an unfinalized or unverifiable upload returns 422 UPLOAD_NOT_VERIFIED; if the eligibility store is unavailable, submission fails closed with 503.
Rate limits
The API enforces per-key rate limits. When a limit is hit, the response is:
HTTP 429 Too Many Requests
Retry-After: 15
Back off for at least Retry-After seconds before retrying. Implement
exponential backoff for long-running polling loops.
Polling pattern (Python)
import time, requests, os
API = "https://kinocloud.io/api/v1"
KEY = os.environ["KINOCLOUD_API_KEY"]
headers = {"Authorization": f"Bearer {KEY}"}
def poll_job(job_id: str, interval: int = 10) -> dict:
while True:
r = requests.get(f"{API}/jobs/{job_id}/status", headers=headers)
r.raise_for_status()
state = r.json()["state"]
if state in ("SUCCEEDED", "COMPLETED", "FAILED", "CANCELLED", "CANCELED"):
return r.json()
time.sleep(interval)
result = poll_job("job_01JABCDE12345")
if result["state"] == "SUCCEEDED":
outputs = requests.get(
f"{API}/jobs/{result['jobId']}/outputs", headers=headers
).json()
for file in outputs["outputs"]:
print(file["filename"], file["sha256"], file["downloadUrl"])
Cancelling a job
curl -X POST https://kinocloud.io/api/v1/jobs/JOB_ID/cancel \
-H "Authorization: Bearer kc_live_YOUR_KEY"
Cancellation is best-effort: if the job has already reached SUCCEEDED or
FAILED, the cancel request returns 200 but has no effect.