Jobs
Every operation that takes real time — rigging a mesh, generating a model — returns a job. Jobs share one status vocabulary, one result envelope, and one polling endpoint, so you write the waiting logic once no matter what you asked for.
The job object
{
"id": "d188878e-b153-4eb7-85e6-e563636e37f5",
"object": "job",
"operation": "rig",
"status": "succeeded",
"output": {
"model_url": "https://cdn.cinevva.com/rigging/d188878e.../model.glb",
"thumbnail_url": null
},
"error": null,
"created_at": "2026-08-30T20:57:23.492Z",
"completed_at": "2026-08-30T20:57:56.104Z",
"duration_seconds": 32.6
}| Field | Description |
|---|---|
operation | What was asked for: rig or generate_model |
status | queued, processing, succeeded, or failed |
output | Non-null only when succeeded. Carries model_url and, when the operation produces them, thumbnail_url and files |
error | Non-null only when failed |
operation is a stable verb, not the name of whatever model or vendor served it. That is deliberate: swapping the engine behind an operation should never be a breaking change for you.
Retrieve a job
Poll this until status is terminal. It is the same endpoint whichever operation created the job, so a helper like the one below works for the whole API.
import time, requests
TERMINAL = {"succeeded", "failed"}
def wait(job_id, api_key, timeout=300, interval=5):
headers = {"Authorization": f"Bearer {api_key}"}
deadline = time.time() + timeout
while time.time() < deadline:
job = requests.get(
f"https://api.cinevva.com/v1/jobs/{job_id}", headers=headers, timeout=30
).json()
if job["status"] in TERMINAL:
if job["status"] == "failed":
raise RuntimeError(job["error"])
return job["output"]["model_url"]
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")Poll every 5 seconds for a Fast rig and every 15 to 20 for a Pro rig or a generation. Polling costs no credits, but it does count against your rate limit.
GET /v1/rigs/{id} still works as an alias for rig jobs, and returns the identical object.
List jobs
Your job history, newest first.
| Parameter | Description |
|---|---|
operation | Filter to rig or generate_model |
status | Filter to queued, processing, succeeded, or failed |
limit | Default 20, max 100 |
offset | Pagination offset |
curl "https://api.cinevva.com/v1/jobs?operation=rig&status=succeeded&limit=10" \
-H "Authorization: Bearer $CINEVVA_API_KEY"{ "object": "list", "total": 57, "jobs": [ /* … */ ] }This lists jobs created through this API. Work you did in the Cinevva web app on features with no API surface is not included, because listing something you cannot reproduce here would be misleading.
Chaining
from_job on POST /v1/rigs takes the output of a previous job as its input, so a generated model can go straight into rigging without a download and re-upload:
# 1. Generate
GEN=$(curl -s -X POST https://api.cinevva.com/v1/models \
-H "Authorization: Bearer $CINEVVA_API_KEY" -H "Content-Type: application/json" \
-d '{"image_url":"https://example.com/knight.png"}' | jq -r .id)
# 2. …wait for it to succeed, then rig it by id
curl -X POST https://api.cinevva.com/v1/rigs \
-H "Authorization: Bearer $CINEVVA_API_KEY" -H "Content-Type: application/json" \
-d "{\"from_job\":\"$GEN\",\"rig_type\":\"biped\"}"The source job must have succeeded; chaining off one that is still running returns invalid_request telling you so, rather than failing later with a confusing input error.
Because from_job reads the previous job's output rather than anything rig-specific, the same field will accept future operations as they land.
Failed jobs are not HTTP errors
A job that is accepted and then fails returns 200 with status: "failed" and a populated error. Only the submission itself produces a 4xx or 5xx. See errors.