NodaroAccount
Nodaro Docs
DocumentationNode ReferenceModelsTemplatesAI Agents (MCP)DevelopersSelf-hostingResearchCompare
REST API

Jobs

Poll Nodaro jobs for status and results, filter your job list, rate and favorite results, read image sizes, check 100 jobs in one call and cancel jobs.

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.
PUT/v1/jobs/:id/ratingRate one of your jobs good or bad, or clear the rating.
PATCH/v1/jobs/:idName one of your jobs, or clear its name.
POST/v1/jobs/from-uploadRecord an image edited in your own app as a job.
POST/v1/gallery/favoriteFavorite a job, or remove the favorite.
GET/v1/gallery/favoritesYour favorited job ids.
POST/v1/jobs/:id/cancelCancel a job and release its reserved credits.
DELETE/v1/jobs/:idSend a job to the Trash. ?forever=true deletes a job already in the Trash for good.
POST/v1/jobs/:id/restoreBring a deleted job back from the Trash.
GET/v1/trashYour Trash: removed session items and deleted jobs.
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. To have Nodaro tell your server when a job ends instead of polling, register a webhook endpoint.

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 five kinds of failure. See Why a job failed.
error_detailWhat the provider itself said when the job failed, or null. See The provider's own words.
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 and list. The credits the job held when it was accepted. This amount never changes.
credits_chargedFull job and list. What the job cost once its hold settled, the steps it ran included, or null. See Credits of a job.
credits_held, credits_releasedFull job and list. The run's hold with its steps' holds, and what went back from it once every hold settled. See Credits of a 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.
session_idFull job and list. The generation session the job was filed in, or null.
favorited, ratingFull job and list. Your favorite (true or false) and your rating ("good", "bad" or null). See Favorites and ratings.
titleFull job and list. Your own name for the job, or null until you name it. See Favorites and ratings.
created_at, started_atFull job only. Timestamps.
completed_atWhen the job finished, on the server's clock, or null until it has. The status route returns it too.

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

An Apply EDL job's output_data says what the render was cut from. quality is "proxy" for a Preview and "final" for a final, and clipKey names the plan clip when the request sent one. planBasis is returned when the request sent it, and renderBasis is a stamp of the render's own settings and sources, written by the server. A Speaker View job returns the same three stamps as sent. An older Apply EDL job reads back with the quality it was ordered at.

Image size on image jobs

Every job whose output_data has an imageUrl also carries the image's size in pixels: generations, edits, image-to-image runs, collages, overlays, extracted frames and more. A client can lay out a grid before the images load, or show and sort by size.

FieldTypeMeaning
width, heightintegerThe size of imageUrl in pixels, as the file is stored: what you download, a free-tier watermark included. EXIF orientation is applied, so it is the size a browser shows.
images{ url, width, height }[]Beside imageUrls, on a job that made several images: one entry per image, in imageUrls order. An entry whose size could not be read has only its url. imageUrls itself stays an array of strings.
{
  "imageUrl": "https://cdn.nodaro.ai/images/<jobId>.png",
  "imageUrls": ["https://cdn.nodaro.ai/images/<jobId>.png", "https://cdn.nodaro.ai/images/<jobId>-v1.png"],
  "width": 1024,
  "height": 1536,
  "images": [
    { "url": "https://cdn.nodaro.ai/images/<jobId>.png", "width": 1024, "height": 1536 },
    { "url": "https://cdn.nodaro.ai/images/<jobId>-v1.png", "width": 1024, "height": 1536 }
  ]
}

Treat all three fields as optional. Jobs completed before sizes were recorded carry none of them, and a size the server could not read is left out, never guessed. The fields are on every job read: GET /v1/jobs, GET /v1/jobs/:id and GET /v1/jobs/:id/status. The MCP job tools get_job, wait_for_job and get_asset carry width and height beside outputUrl.

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 five 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.
  • subject: "input_video" appears when the provider refused the job's input video itself, not the prompt, a reference image or the result. Send a different clip.
  • subject: "input_image", always with class: "copyright", appears when the provider refused one of the job's input images as possibly copyrighted. Send a different picture: rephrasing the prompt will not help.
  • subject: "prompt" appears, on any class, when the provider's own words named the prompt. Changing the prompt is the fix.
  • subject: "audio" appears, on any class, when the provider's own words blamed the generated audio, not the picture.
  • Treat a subject you do not know as absent.

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.

When the job ran out of credits partway, because a step that reserves its own credits was refused, for example the video that a Nodaro Motion or trend run starts:

{ "kind": "credits", "code": "insufficient_credits" }
  • code is the refusal's own code, in lowercase snake case: insufficient_credits, when the balance could not cover the step, or a plan or workspace limit such as free_video_limit, instance_cap_reached, budget_exceeded, member_cap_exceeded or user_allowance_exceeded. Treat an unknown code like insufficient_credits.
  • The job's credits are refunded. Ask the person to top up, or to raise the limit, and run it again. The same request fails the same way until then.

The provider refused the job's input video without naming a content-policy class. Its message is about the file, not a policy:

{ "kind": "input-refused", "subject": "input_video" }

Treat it like any other failure. No other model is suggested, but you know which input to change: send a different clip.

The provider refused the request as it was sent, naming a fault of the request, such as a setting, a file's format or size, or the prompt's length:

{ "kind": "request-refused", "subject": "input_video" }

subject: "input_video" is there only when the provider's words name the input video as the fault, for example its length or size, or no character found in it. A different clip is then what can change. The same request is refused the same way, so do not run it again unchanged. Change the input images, the video, the prompt or the settings. It is not a content-policy block. On a job made of steps, such as a Nodaro Motion or trend run, it is the refusal of one step.

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. Sometimes the model refuses the request with no reason, only its general "Generation failed. Please try again later." message. Then error_message says that the model gave no reason, and that the cause may be temporary or in the input media, such as its size or format. Run that job again once, and change the input media if it fails 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.

The provider's own words

Beside error_message, the platform's own sentence about the failure, a failed job carries error_detail: what the provider itself said, stored when the job failed. Links in it are cut to their host, and secrets are removed. Otherwise the words are the provider's own, for example task failed: [400] Request blocked: The uploaded image may contain protected or restricted IP content.

  • null when the provider said nothing, for example on a refusal before anything was sent, a timeout, or a queue wait that ran out. An older server leaves the field out.
  • Show it next to error_message. Branch on error_hint, never on these words.
  • Where it appears: the same job payloads as error_message, and the job inside a job.failed webhook event. A failed node of a workflow run carries it as errorDetail in nodeStates. See Executions.

The words go only to a caller who may read them, because they can quote a prompt:

  • A trend run withholds the words with its text, because they can quote its hidden input.
  • The jobs of a workflow run carry the words only when their owner can open the workflow that the run executed. That is the workflow's creator, or someone it is shared with. You own the jobs of a run you start from someone else's share link, but they carry error_detail: null, on every job payload and in the job.failed event.
  • A published app's runner never receives the words.
  • The hidden run of a use-case node, such as Tighten Episode, executes no saved workflow. When a node of a workflow run started it, it is linked to that run and judged by that run's workflow, on its node states and its jobs alike. A hidden run inside another follows the links to the nearest run of a workflow. A hidden run started on its own is its owner's.
  • A job of your own that belongs to no run, such as one node run on its own, carries its words to you. So does a job that is not linked to the run that started it. That is a job of a few node types, a job of a use-case node's hidden run made before these links were recorded, or a job of a run whose workflow was deleted. A linked hidden run whose run is gone keeps its words from everyone but administrators.
  • A deployment can keep the words for its administrators. With jobs.errorDetail: "admin" in its surface profile, every caller who is not an administrator gets error_detail: null. That is what a failure with no provider words looks like, so a client needs no change. See Editions and surface profiles.

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, GET /v1/jobs/:id, GET /v1/jobs/:id/status and GET /v1/jobs/status, never on 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.

A job's credits is the amount the job itself held when it was accepted, and it never changes. GET /v1/jobs and GET /v1/jobs/:id also carry credits_charged: what the job cost once its hold settled, read from the same credit record.

  • credits_charged can be lower than credits. Some jobs hold more than they use, for example to cover a possible re-render or a length measured after the run. The rest goes back to your balance.
  • When the job's own hold was refunded, credits_charged is 0 plus any steps that settled. For example, a refunded Nodaro Motion run whose reference and mask steps were charged reads credit_status: "refunded" with credits_charged: 165.
  • credits_charged is null while the job's own hold is still open (credit_status: "reserved"), even when some of its steps have already settled. It is also null when nothing in the run was reserved.

To total what jobs cost, add up credits_charged, not credits. For example, a job that held 888 credits and settled at 444 reads credits: 888, credits_charged: 444 and credit_status: "committed".

Jobs that run other jobs

credits_charged is the charge of the job as a run, its steps included. Some jobs run other jobs for you. For example, a Nodaro Motion run prepares its references, makes a mask and renders the video, each as a step of its own. A step is never listed on its own, and it is charged once it settles.

  • A step whose hold is still open adds nothing yet, and a refunded step adds 0.
  • credits_held is the run's hold: the job's own credits plus its steps' holds. For a job with no steps, it is the same as credits.
  • credits_released is what went back from the hold (credits_held minus credits_charged), once every hold in the run has settled. It is null until then.
  • credits and credit_status stay the job's own.
  • If the steps cannot be read for a moment, credits_charged, credits_held and credits_released read null. That means unknown, never a lower figure.

For example, a Nodaro Motion preview whose own hold was 44, with two reference steps at 66, a mask at 33 and a video held at 3,564 and settled at 854, reads credits: 44, credits_charged: 1063, credits_held: 3773 and credits_released: 2710.

A workflow run counts a node the same way. The node's credits include the settled steps of its job, a failed node's too, and so does the run's totalCreditsUsed. A step that settles after its node finished is counted when the run ends.

List your jobs

GET /v1/jobs returns your jobs, newest first, as { data: Job[], next }. A child job, one that another job started as a step of its own work, is not listed. Output keys whose name starts with __ are for the server only, and no job read returns them.

QueryMeaning
limitPage size, up to 100.
cursorThe next value of the previous page.
statusOne or more job statuses: pending, queued, processing, pending_review, completed, failed or cancelled. An unknown status answers 400 validation_error.
typeOne or more routes that created the job, such as llm-structured or video-analysis, up to 20.
originThe client app that sent it, exactly, such as studio.
sourceOne or more surfaces that created the job: mcp, cli, sdk, web, extension, app, api or internal, the job's source field. source=mcp lists what your MCP clients, such as Claude or ChatGPT, made.
favoritedtrue keeps the jobs you favorited, and false the ones you did not.
ratinggood, bad, rated (any rating) or unrated.
projectIdThe jobs a session of this project holds now, each listed once. none keeps the jobs a session with no project holds. Cannot be combined with inSession=false.
inSessionfalse keeps the jobs no session holds now, which the app shows as General. true keeps the jobs at least one session holds.
withSessionstrue adds sessions: [{ id, title, projectId }] to each job: the sessions it is in.
batchIdThe jobs of one Generate Image batch. See Several images in one call.
attachToCharacterIdThe jobs of one character. See Characters.

status, type and source take a comma-separated list (status=failed,cancelled) or a repeated parameter (type=a&type=b). Every filter matches exactly, and the filters 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' })

Favorites and ratings

You can keep two marks on your generations. Both are readable on every job and usable as list filters:

  • A favorite is a bookmark on a job: one of your own, in any status, or anyone's public completed job in the gallery.
  • A rating is your verdict on one of your own jobs: good or bad.

You can also give one of your jobs a title, a name of your own, such as "Wedding toast, final" on a picture, a clip, a track or a speech take.

MethodPathBody or queryWhat it does
PUT/v1/jobs/:id/rating{ "rating": "good" | "bad" | null }Rate one of your jobs, or clear its rating with null. rating is required, so clearing is explicit. Returns { data: { jobId, rating } }.
PATCH/v1/jobs/:id{ "title": string | null }Name one of your jobs, or clear its name with null or "". Returns { data: { jobId, title } }.
POST/v1/gallery/favorite{ "jobId": "..." }Favorite a job, or remove the favorite when it already is one. Returns { favorited }, the state after the toggle.
GET/v1/gallery/favoritesNoneYour favorited job ids, newest first: { data: string[] }.
GET/v1/gallerytype, userId, favoritesOnly=true, limit (1 to 50, default 20), cursorThe public gallery, newest completed first. With your own userId and your token, favoritesOnly=true narrows the list to your favorites. Each card carries title, the owner's name for the job, or null.
  • Every job carries favorited and rating on GET /v1/jobs and GET /v1/jobs/:id. When the server cannot read them for a moment, the jobs still answer and the two fields are left out. Treat a missing field as unknown, never as false: a toggle on an unknown favorite could remove it.
  • One rating per job, its owner's. Anyone else's job answers 404 not_found. A rating other than good, bad or null answers 400 validation_error.
  • Another person's private job answers 404 not_found on a favorite, and nothing is stored. Removing a favorite always works.
  • Scopes: an OAuth app token needs assets:write for the two writes, and assets:read or jobs:read to list your favorites. The job reads stay jobs:read.
  • A deployment that hides the gallery answers 404 on every /v1/gallery route.
  • A deployment that does not allow public work answers every GET /v1/gallery view with an empty page, { data: [], nextCursor: null, totalCount: 0 }, except your own: your own userId with your token still lists your work. There, POST /v1/gallery/favorite adds only your own jobs, and anyone else's job answers 404 not_found. Removing a favorite always works. The gallery's report route, POST /v1/gallery/report, answers 404 not_found for every item.
  • A title is one line of plain text, 1 to 120 characters, in any language, trimmed. Anything else answers 400 validation_error. Only the job's owner can name it, and anyone else's job answers 404 not_found. Naming is free and works on any job type and status, the Trash included. Nothing else about the job changes. An OAuth app token needs assets:write.
  • The title reads back as title wherever the job is returned: GET /v1/jobs/:id, GET /v1/jobs, a session's items, GET /v1/trash, the gallery's cards (public, beside the prompt) and the library's rows. On a library row, title is the name of the job that made the file, not its filename.
# Rate a job good, favorite it, then list your good favorites that completed
curl -s -X PUT "https://app.nodaro.ai/v1/jobs/$JOB/rating" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"rating": "good"}'
curl -s -X POST "https://app.nodaro.ai/v1/gallery/favorite" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"jobId\": \"$JOB\"}"
curl -s "https://app.nodaro.ai/v1/jobs?rating=good&favorited=true&status=completed" \
  -H "Authorization: Bearer $NODARO_API_KEY" | jq '.data[] | {id, favorited, rating}'

Record an edited image

An image edited in your own app, for example cropped, annotated or filtered, is an upload, not a job. On its own, it cannot be rated, favorited, filed in a session or passed to a tool that takes a job id. Record it as a job:

  1. Store the edited image with POST /v1/upload, and keep its assetId. See Uploads.
  2. Optionally, store the editor's design state with POST /v1/upload-json, so the edit can be opened again. It takes any JSON body up to 25 MB and returns { url }.
  3. Call POST /v1/jobs/from-upload with { assetId, sourceJobId?, sourceImageUrl?, sourceAssetId?, editStateUrl?, sessionId? }.

The route records the upload as an image-edit job that is completed at once, and returns { data: Job }. It answers 201 when it records the image, and 200 when this assetId was recorded before, with the same job.

  • Free and private. Recording the job charges nothing, because the file's storage was counted when you uploaded it. The job is never public: it lists in your own job history, gallery and library views, never in the public gallery.
  • What it holds. output_data.imageUrl is the upload, with thumbnailUrl, width and height. input_data carries type: "image-edit", the assetId and the source: sourceJobId or sourceAssetId, plus sourceImageUrl, the source's image. For a job that made several images, send the one you edited as sourceImageUrl. An edit of a character, object, creature or location image keeps that link, so it lists with that asset. GET /v1/jobs?type=image-edit lists these jobs.
  • One upload, one job. The same assetId again answers the same job, so a retry or a double click never makes two. A retry also finishes a job that an earlier attempt left unfinished, so a 2xx answer always carries a completed job. The job keeps the source, design state and session of the request that created it. A retry's own values are not applied. To show the job in a second session, add it there with POST /v1/sessions/:id/items.
  • Refusals. 404 not_found when the asset, the source or the session is not yours. 400 validation_error when the asset is not an image or is not a file you stored. The same error comes when the source job has no finished image, sourceImageUrl is not one of its images, both sources are sent, or editStateUrl is not a URL that POST /v1/upload-json returned. 409 conflict when you already used image-edit:<assetId> as the Idempotency-Key of another request. 422 job_blocked when a job policy of the deployment refuses the image, on every retry.
  • Deleting. Deleting the job leaves the uploaded file in your uploads. Deleting the upload from your library keeps the file while this job still shows it.

An OAuth app token needs assets:write.

# Record an edited image next to the job it was made from, in a session
curl -s -X POST "https://app.nodaro.ai/v1/jobs/from-upload" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"assetId\": \"$ASSET\", \"sourceJobId\": \"$JOB\", \"sessionId\": \"$SESSION\"}" | jq '.data | {id, status, output_data}'

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. Cancelling a job also cancels its child jobs that are still running. A finished child job is left as it is, and one already sent to the model finishes, like any job in flight.

Cancelling a metered LLM job. A language-model job is charged for the tokens its call uses, so cancelling it depends on whether the call has started:

  • POST /v1/jobs/:id/cancel on a job whose call has started answers { "success": true, "cancelled": 0, "inFlight": true }. The job finishes and is charged only the credits it used.
  • The canvas Stop, POST /v1/jobs/cancel-all and a workflow cancel (POST /v1/workflow-executions/:id/cancel) cancel the job. It is charged what the provider already billed for it, and the rest of its hold is refunded.
  • A job stopped before its call starts is refunded, whichever way it was cancelled. The exception is what an earlier attempt of the same job already used, for example a retry after a billed attempt. That usage is charged.

DELETE /v1/jobs/:id sends a job to the Trash and answers { "success": true }. Only the job's owner may delete it. A job that is still running keeps running, so cancel it first when its work should stop. Nothing is refunded when a job is deleted. OAuth apps need the assets:write scope to delete or restore a job.

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.

The Trash

Removing an item from a session and deleting a job both send it to the Trash.

  • A removed item stays until you restore it with POST /v1/sessions/:id/items/:jobId/restore, or delete it for good with DELETE /v1/sessions/:id/items/:jobId?forever=true. A removal never deletes the job. An item that is in no live session shows in General.
  • A deleted job disappears from every read: job lists and reads by id, status polls, the gallery, session feeds and previews, MCP tools and the CLI. It stays restorable for 30 days. POST /v1/jobs/:id/restore puts it back in every session it was in.
  • After 30 days a deleted job is deleted for good, with its private media. A copy saved to the Library stays.
  • Delete for good now. DELETE /v1/jobs/:id?forever=true deletes a job that is already in the Trash for good. On a live job it answers 409 not_in_trash.
  • Nothing is refunded when a job is deleted, or when it is deleted for good.

GET /v1/trash lists the Trash, newest first, one page at a time (limit 1 to 100, default 50, and cursor). Each entry is either { kind: "removed", removedAt, session: { id, title, projectId }, job } or { kind: "deleted", deletedAt, purgeAt, job }. A removed item whose job you also deleted is listed once, as the deleted job.

QueryMeaning
projectIdA project id: that project's sessions' removed items, and the deleted jobs a session of it held. none: the sessions with no project, and General.
toolWhat a session of that creation tool holds or held. A job no session ever held has no tool, so tool leaves it out.
typeThe job's input_data.type, comma-separated. Filter by type alone, with your tool's job types, to include deleted jobs that were never in a session.

Deleting a session also deletes its removed items.

Component runs

POST /v1/component/execute runs a saved Component in the background and answers 202 with { jobId }. Poll that job like any other. On a deployment that does not allow public work, it runs only your own components, or a component inside a full run of a workflow that holds it. Any other call answers 404 not_found "Component not found". With an OAuth app token, the route needs the workflows:execute scope, and the wait-limit read below needs jobs:read. 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": 760, "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

What is a job in the Nodaro API?

A job is one unit of generation, such as one image, one video render or one speech clip. POST /v1/<node-type> returns a job id, and a workflow run creates one job for each AI node it runs.

How often should I poll a Nodaro job?

Every 2 to 5 seconds. A job changes state in seconds, not milliseconds. To track many jobs, use GET /v1/jobs/status or POST /v1/jobs/batch-status, which return up to 100 jobs in one call.

What does the pending_review status mean?

The deployment holds the result for a human review before releasing it. The job is still in flight and its credits stay reserved. Keep polling; it ends completed, failed or cancelled.

How do I know if a failed job was refunded?

Read credit_status on GET /v1/jobs/:id or GET /v1/jobs/:id/status. It is reserved, committed or refunded. A generation blocked by a safety filter or a deployment policy is always refunded.

How do I know what a job actually cost?

Read credits_charged on GET /v1/jobs or GET /v1/jobs/:id. It is what the job cost once its hold settled, including the steps it ran, and it can be lower than the hold. It is null while the job's own hold is still open.

Can I rate or favorite a job through the API?

Yes. PUT /v1/jobs/:id/rating sets good, bad or null on one of your jobs, and POST /v1/gallery/favorite toggles a favorite. GET /v1/jobs filters on both with rating= and favorited=.

Last updated on

On this page