Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
REST API

Jobs

Poll Nodaro jobs for status and results, read failure hints and credit status, check 100 jobs in one call, cancel jobs, and stop or continue Video Pro runs.

A job is one unit of generation in Nodaro: one image, one video render, one speech clip. A node run returns a job id, a workflow run creates one job for each AI node, and the job endpoints report each job's status, progress, result and credits. You poll a job until it reaches completed, failed or cancelled, then read its result from output_data.

Endpoints

MethodPathWhat it does
GET/v1/jobs/:id/statusThe lean status for polling: status, progress, result and error.
GET/v1/jobs/:idThe full job, including what was sent (input_data) and its credits.
GET/v1/jobsYour jobs, newest first, in pages.
GET/v1/jobs/status?ids=…The status of up to 100 jobs, ids in the query.
POST/v1/jobs/batch-statusThe status of up to 100 jobs, ids in the body.
POST/v1/jobs/:id/cancelCancel a job and release its reserved credits.
DELETE/v1/jobs/:idDelete a job and the private media it produced.
GET/v1/component/execute/:jobId/wait-limitHow long the server waits for a component run.
POST/v1/generate-video-pro/:jobId/stopStop a Generate Video Pro run and keep its finished segments.
POST/v1/generate-video-pro/continueContinue a Generate Video Pro run as a new job.
POST/v1/credits/video-pro-estimatePrice a Generate Video Pro run without starting it.

An OAuth token needs the jobs:read scope to read jobs. Personal API tokens need no scope.

Read a job

curl -s https://app.nodaro.ai/v1/jobs/0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10/status \
  -H "Authorization: Bearer $NODARO_API_KEY"
{
  "data": {
    "id": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
    "status": "completed",
    "progress": 100,
    "output_data": { "imageUrl": "https://…/0f1a9c2e.png" },
    "error_message": null,
    "error_hint": null,
    "credit_status": "committed"
  }
}
const { data } = await client.jobs.getStatus(jobId)
if (data.status === 'completed') console.log(data.output_data)

// The full record, with input_data and credits:
const { data: job } = await client.jobs.get(jobId)
nodaro jobs get 0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10 --json

GET /v1/jobs/:id/status is made for poll loops: it skips input_data and the cost fields, so it is lighter than GET /v1/jobs/:id. Both answer { "data": … }, and both answer 404 for a job that does not exist or is not yours. Job fields use snake_case, as the server sends them.

FieldMeaning
idThe job id.
statusWhere the job is. See Job statuses.
progress0 to 100.
output_dataThe result. Media jobs carry imageUrl, videoUrl or audioUrl, often with thumbnailUrl. Other jobs carry their own fields, such as text or json.
error_messageWhy the job failed, in words.
error_hintA structured reason for two kinds of failure. See Why a job failed.
credit_statusreserved, committed, refunded or null. See Credits of a job.
recoveringtrue while the platform repairs a job whose worker stopped after the model had delivered.
input_dataFull job only. What was sent, after corrections: for example the final prompt, your own userPrompt, and the direction ids.
creditsFull job only. The credits of the job.
job_type, source, source_detailFull job only. The kind of job, and where it came from: source is internal, mcp, app, cli, sdk, extension, web or api, and source_detail names the client, such as sdk/1.10.0.
created_at, started_at, completed_atFull job only. Timestamps.

input_data and output_data are public views: fields that exist only for the server are removed for every caller.

Job statuses

StatusFinalMeaning
pending, queued, processingNoThe job is waiting to run or running.
pending_reviewNoThe result exists, and the deployment holds it for a human review. Keep polling.
completedYesThe result is in output_data.
failedYeserror_message and error_hint say why.
cancelledYesYou or the platform cancelled the job.

Recovering jobs. When a worker stops after the model has already delivered, the job stays processing with recovering: true while the platform repairs it. It then completes or is refunded on its own. For slow models this can take tens of minutes, which is longer than the SDK's default wait: a JobTimeoutError from runAndWait does not cancel the job, so fetch it again later.

Jobs held for review. On a deployment that registers a review policy, a job can enter pending_review. The work is done, the credits stay reserved for the whole hold, and a person decides whether the result is released. The job then ends completed (approved), failed with a policy-block hint (rejected) or cancelled. A held job can be cancelled like any job in flight. Do not run the request again, because a duplicate would be held too. When the deployment sets a review deadline, a job nobody reviews in time is rejected: it ends failed, the reservation is refunded and the held result is deleted. A held job is never approved automatically. The SDK's runAndWait throws JobHeldError on the first poll that sees pending_review, and the CLI's --watch exits with code 3.

Poll well

  • Poll every 2 to 5 seconds. A job changes state in seconds, not milliseconds.
  • Status reads are free of the token's rate limit. Only workflow runs and workflow lists count against a personal API token's per-minute limit. See Rate limits.
  • Track many jobs with one call. Use the batch endpoints instead of one request per job.
  • Let the client wait for you. The SDK's client.nodes.runAndWait polls every 2 seconds for up to 15 minutes, and the CLI's --watch polls until the job ends.

Why a job failed

The error table in Errors covers requests that never created a job. A job that fails later carries error_message, and for two kinds of failure also a structured error_hint.

A model's safety filter blocked the request:

{ "kind": "safety-block", "class": "safety", "retried": true, "suggestedProvider": "nano-banana-pro" }
  • class is copyright, likeness or safety. A copyright or likeness block is final: the same request never passes.
  • A safety filter is not always consistent on some models. For GPT Image 2, GPT Image 2.5 Flare and GPT Image 2.5 Sunburst, Nodaro retries the same request once, at no extra cost. retried says whether that retry already ran.
  • suggestedProvider appears only when the model has a recommended fallback. It is a real model id: send the same prompt and references to it.

A deployment's policy rejected the job:

{ "kind": "policy-block", "policyId": "brand-safety", "reason": "This image was not approved for release.", "hookPoint": "result" }
  • reason is text written for your user by the deployment's policy. Show it as it is.
  • hookPoint is request when the job was refused before it ran, and result when its result was rejected afterwards, including by a reviewer.
  • The platform does not retry a policy rejection and offers no other model.

Every other failure has no error_hint. When a model refuses the request itself, because the settings or the input media are invalid for that model, error_message says so: change the settings or the media before you run it again. An error on the model's side stays worth retrying, whatever its wording. error_hint appears on every job payload that carries error_message: the full job, the status routes, the list, and both batch routes.

Credits of a job

credit_status follows the job's credit reservation:

ValueMeaning
reservedCredits are held while the job runs.
committedThe job delivered and was charged.
refundedThe reservation was released, for example after a safety block.
nullThe job has no credit record to report.

It appears on GET /v1/jobs/:id, GET /v1/jobs/:id/status and GET /v1/jobs/status, never on GET /v1/jobs or POST /v1/jobs/batch-status. A generation that ends in a safety block or a policy block is always refunded. The rare exception is a job whose credits were already settled before a policy rejected its result. See Credits.

List your jobs

GET /v1/jobs returns your jobs, newest first, as { data: Job[], next }:

QueryMeaning
limitPage size, up to 100.
cursorThe next value of the previous page.
typeThe route that created the job, exactly, such as llm-structured or video-analysis.
originThe client app that sent it, exactly, such as studio.
attachToCharacterIdThe jobs of one character. See Characters.

type and origin combine. A page may hold fewer rows than limit, even none, and still carry a next: keep paging while next is present, never by counting rows.

const { data: runs, next } = await client.jobs.list({ type: 'llm-structured', origin: 'my-app' })

Poll many jobs at once

Two endpoints return the status of up to 100 jobs in one round trip:

curl -s "https://app.nodaro.ai/v1/jobs/status?ids=$JOB_A,$JOB_B,$JOB_C" \
  -H "Authorization: Bearer $NODARO_API_KEY"
{
  "jobs": [
    { "id": "0f1a9c2e-…", "status": "completed", "output_data": { "imageUrl": "https://…/a.png" }, "error_message": null, "error_hint": null, "credit_status": "committed" },
    { "id": "7d3e5f60-…", "status": "processing", "output_data": null, "error_message": null, "error_hint": null, "credit_status": "reserved" }
  ]
}
curl -s -X POST https://app.nodaro.ai/v1/jobs/batch-status \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"jobIds\": [\"$JOB_A\", \"$JOB_B\", \"$JOB_C\"]}"

The answer is { "data": [ … ] }, with id, status, output_data, error_message and error_hint for each job, and no credit_status.

Ids that do not exist, or that belong to someone else, are left out without an error. Compare the answer with your own list of ids.

Cancel or delete a job

POST /v1/jobs/:id/cancel cancels a job and releases the credits it reserved. It answers { "success": true, "cancelled": 1 }. A job held in pending_review can be cancelled too.

DELETE /v1/jobs/:id deletes a job and the private media it produced, and answers { "success": true }. Only the job's owner may delete it. A running job is deleted as it is, so cancel it first when its work should stop.

curl -s -X POST https://app.nodaro.ai/v1/jobs/$JOB_ID/cancel \
  -H "Authorization: Bearer $NODARO_API_KEY"
const { cancelled } = await client.jobs.cancel(jobId)
await client.jobs.delete(jobId)
nodaro jobs cancel 0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10

To stop a whole workflow run, cancel its execution instead: see Executions.

Component runs

POST /v1/component/execute runs a saved Component in the background and answers 202 with { jobId }. Poll that job like any other. The server gives a component run 90 minutes, plus the time budget of any long render inside it, such as the final render of an Apply EDL node.

A client with its own time limit can ask how long the server will wait:

curl -s https://app.nodaro.ai/v1/component/execute/$JOB_ID/wait-limit \
  -H "Authorization: Bearer $NODARO_API_KEY"
{ "data": { "budgetExcessMs": 0, "waitLimitMs": 5400000, "pendingBudgetedNodes": true } }
  • waitLimitMs is the server's wait for this run: 90 minutes plus budgetExcessMs.
  • budgetExcessMs stays 0 until the run starts a long render.
  • pendingBudgetedNodes is true while a long render has not started yet, and while the run itself has not started. So a 0 excess does not yet mean there is nothing long inside.

Both values can change while the run goes on, so ask again when waitLimitMs is reached. The route answers only the run's owner, with 404 for anyone else and for jobs that are not component runs. The editor itself uses this rule: it waits 30 minutes for a run with nothing long inside, waitLimitMs once a long render has started, and at least 90 minutes while pendingBudgetedNodes is true, then asks again.

Generate Video Pro runs

Generate Video Pro makes long videos one segment at a time and saves its progress between segments, so a run can be stopped and continued later. It runs on Nodaro Cloud, and a self-hosted install runs it through its connection to Nodaro Cloud.

Stop a run

POST /v1/generate-video-pro/:jobId/stop stops a processing run gracefully:

  • The segment being generated is abandoned, and it is still billed, because the model keeps rendering it.
  • The remaining segments are skipped.
  • Every finished segment is stitched into the job's final video.
  • The unused part of the reservation is refunded.

The answer is { "jobId": "…", "stopping": true }. Keep polling the job: it ends completed, with output_data.pro.stopped set to true and stoppedAtSegment. A job that is still pending is cancelled with a full refund instead.

Continue a run

POST /v1/generate-video-pro/continue starts a new job from a finished one. It reuses the parent's plan and every delivered segment before fromSegment, and generates again from there:

{ "fromJobId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "fromSegment": 4 }
  • fromSegment counts from 1. By default it is the first segment that was not delivered.
  • The parent must be finished: stopped, failed with at least one delivered segment, or completed. An explicit fromSegment on a completed run generates its ending again.
  • You pay only for the segments generated again, plus the flat Video Pro fee.
  • The route honors an Idempotency-Key header, so a retried request does not start a second job.

The answer is { jobId, continuedFromJobId, fromSegment, segmentCount }. Poll the new jobId.

curl -s -X POST https://app.nodaro.ai/v1/generate-video-pro/$JOB_ID/stop \
  -H "Authorization: Bearer $NODARO_API_KEY"

curl -s -X POST https://app.nodaro.ai/v1/generate-video-pro/continue \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4c1f0a7e-2b3d-4e5f-8a9b-0c1d2e3f4a5b" \
  -d "{\"fromJobId\": \"$JOB_ID\", \"fromSegment\": 4}"
await client.videoPro.stop(jobId)
const { data } = await client.jobs.getStatus(jobId) // completes with a partial video

const { jobId: childId } = await client.videoPro.continueRun(jobId, { fromSegment: 4 })
nodaro video-pro stop 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d
nodaro video-pro continue 9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d --from-segment 4 --watch

Both routes answer 404 for a job that is not yours and 400 for a job that is not a Video Pro run.

Estimate a run

POST /v1/credits/video-pro-estimate prices a Video Pro run without creating a job or reserving credits:

{ "provider": "gemini-omni-flash", "resolution": "720p", "duration": 12, "renderMethod": "keyframes", "segmentMode": "short" }

In one example configuration the answer is { "data": { "credits": 660, "upperBound": true } }. Read the live response for current prices.

Prop

Type

  • With segmentMode short or long, the run first assigns whole actions to spans of the source, and upperBound: true means the figure is the reservation limit before planning. The final charge follows the actual plan.
  • A planOnly estimate covers the planning fee and returns upperBound: false. Its sourceSegmentDurations and planCheckpoint can be sent back as sourceSegmentDurations and seedPlan, with the same mode and settings.

Read Generate Video Pro for how segmentation works and what each setting does.

Frequently asked questions

Last updated on

On this page