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 → 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: SUBMITTED → RUNNING → SUCCEEDED | FAILED | CANCELLED
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 jobs for Blender, Maya, Nuke, Houdini, V-Ray, etc.
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: molecular docking (AutoDock Vina), sequence alignment (BLAST), protein folding (AlphaFold inference), coastal wave simulation (SWAN/WAVEWATCH III), Monte Carlo sampling, image processing pipelines.
Required fields: sceneFile (the input dataset), simulator, taskCount
{
"jobType": "simulation",
"sceneFile": "uploads/neuwave/swan-case.tar.zst",
"simulator": "swan",
"taskCount": 32,
"jobName": "neuwave-swan-hindcast"
}
Supported simulators: autodock-vina, blast, alphafold, swan, wavewatch3
Tightly-coupled MPI workloads (GROMACS, OpenFOAM, WRF) are not yet supported.
Cost estimation
Always estimate before submitting expensive jobs.
curl -X POST https://app.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 requests, os
API = "https://app.kinocloud.io/api/v1"
KEY = os.environ["KINOCLOUD_API_KEY"]
# 1. Presign
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")})
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. Submit using the returned s3Key
r = requests.post(f"{API}/jobs/submit",
headers={"Authorization": f"Bearer {KEY}"},
json={"jobType": "render",
"sceneFile": upload["s3Key"],
"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://app.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 cached result is returned.
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://app.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()
status = r.json()["status"]
if status in ("SUCCEEDED", "FAILED", "CANCELLED"):
return r.json()
time.sleep(interval)
result = poll_job("job_01JABCDE12345")
if result["status"] == "SUCCEEDED":
outputs = requests.get(
f"{API}/jobs/{result['jobId']}/outputs", headers=headers
).json()
for file in outputs["files"]:
print(file["name"], file["downloadUrl"])
Cancelling a job
curl -X POST https://app.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.