# 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.

Source: https://nodaro.ai/docs/developers/api/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

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/jobs/:id/status` | The lean status for polling: status, progress, result and error. |
| `GET` | `/v1/jobs/:id` | The full job, including what was sent (`input_data`) and its credits. |
| `GET` | `/v1/jobs` | Your 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-status` | The status of up to 100 jobs, ids in the body. |
| `POST` | `/v1/jobs/:id/cancel` | Cancel a job and release its reserved credits. |
| `DELETE` | `/v1/jobs/:id` | Delete a job and the private media it produced. |
| `GET` | `/v1/component/execute/:jobId/wait-limit` | How long the server waits for a component run. |
| `POST` | `/v1/generate-video-pro/:jobId/stop` | Stop a Generate Video Pro run and keep its finished segments. |
| `POST` | `/v1/generate-video-pro/continue` | Continue a Generate Video Pro run as a new job. |
| `POST` | `/v1/credits/video-pro-estimate` | Price 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**

```bash
curl -s https://app.nodaro.ai/v1/jobs/0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10/status \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{
"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"
}
}
```

**TypeScript SDK**

```ts
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)
```

**CLI**

```bash
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.

| Field | Meaning |
| --- | --- |
| `id` | The job id. |
| `status` | Where the job is. See [Job statuses](#job-statuses). |
| `progress` | 0 to 100. |
| `output_data` | The result. Media jobs carry `imageUrl`, `videoUrl` or `audioUrl`, often with `thumbnailUrl`. Other jobs carry their own fields, such as `text` or `json`. |
| `error_message` | Why the job failed, in words. |
| `error_hint` | A structured reason for two kinds of failure. See [Why a job failed](#why-a-job-failed). |
| `credit_status` | `reserved`, `committed`, `refunded` or `null`. See [Credits of a job](#credits-of-a-job). |
| `recovering` | `true` while the platform repairs a job whose worker stopped after the model had delivered. |
| `input_data` | Full job only. What was sent, after corrections: for example the final `prompt`, your own `userPrompt`, and the `direction` ids. |
| `credits` | Full job only. The credits of the job. |
| `job_type`, `source`, `source_detail` | Full 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_at` | Full 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

| Status | Final | Meaning |
| --- | --- | --- |
| `pending`, `queued`, `processing` | No | The job is waiting to run or running. |
| `pending_review` | No | The result exists, and the deployment holds it for a human review. Keep polling. |
| `completed` | Yes | The result is in `output_data`. |
| `failed` | Yes | `error_message` and `error_hint` say why. |
| `cancelled` | Yes | You 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](https://nodaro.ai/docs/developers/api/rate-limits).
- **Track many jobs with one call.** Use the [batch endpoints](#poll-many-jobs-at-once) 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](https://nodaro.ai/docs/developers/api/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:**

```json
{ "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](https://nodaro.ai/docs/models/image/gpt-image-2), [GPT Image 2.5 Flare](https://nodaro.ai/docs/models/image/gpt-image-2-5-flare) and [GPT Image 2.5 Sunburst](https://nodaro.ai/docs/models/image/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:**

```json
{ "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:

| Value | Meaning |
| --- | --- |
| `reserved` | Credits are held while the job runs. |
| `committed` | The job delivered and was charged. |
| `refunded` | The reservation was released, for example after a safety block. |
| `null` | The 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](https://nodaro.ai/docs/developers/api/credits).

## List your jobs

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

| Query | Meaning |
| --- | --- |
| `limit` | Page size, up to 100. |
| `cursor` | The `next` value of the previous page. |
| `type` | The route that created the job, exactly, such as `llm-structured` or `video-analysis`. |
| `origin` | The client app that sent it, exactly, such as `studio`. |
| `attachToCharacterId` | The jobs of one character. See [Characters](https://nodaro.ai/docs/developers/api/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.

```ts
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:

**GET /v1/jobs/status**

```bash
curl -s "https://app.nodaro.ai/v1/jobs/status?ids=$JOB_A,$JOB_B,$JOB_C" \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{
"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" }
]
}
```

**POST /v1/jobs/batch-status**

```bash
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**

```bash
curl -s -X POST https://app.nodaro.ai/v1/jobs/$JOB_ID/cancel \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts
const { cancelled } = await client.jobs.cancel(jobId)
await client.jobs.delete(jobId)
```

**CLI**

```bash
nodaro jobs cancel 0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10
```

To stop a whole workflow run, cancel its execution instead: see [Executions](https://nodaro.ai/docs/developers/api/executions).

## Component runs

`POST /v1/component/execute` runs a saved [Component](https://nodaro.ai/docs/nodes/automate/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](https://nodaro.ai/docs/nodes/video/apply-edl) node.

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

```bash
curl -s https://app.nodaro.ai/v1/component/execute/$JOB_ID/wait-limit \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

```json
{ "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](https://nodaro.ai/docs/nodes/video/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:

```json
{ "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**

```bash
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}"
```

**TypeScript SDK**

```ts
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 })
```

**CLI**

```bash
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:

```json
{ "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.

<TypeTable
type={{
provider: { type: 'string', description: 'The video model.', required: true },
resolution: { type: 'string', description: 'The resolution.', default: '720p' },
duration: { type: 'integer', description: 'Total length in seconds, from 1 to 3600.', default: '8' },
aspectRatio: { type: 'string', description: 'The frame shape.' },
renderMethod: { type: 'string', description: 'extend or keyframes.' },
anchorMode: { type: 'string', description: 'upfront, progressive or none.' },
contextTailSec: { type: 'number', description: 'From 2 to 15 seconds.' },
segmentMode: { type: 'string', description: 'short, long or max. Cannot be combined with preferredSegmentSec or segmentDurations.' },
preferredSegmentSec: { type: 'integer', description: 'From 4 to 15 seconds.' },
segmentDurations: { type: 'integer[]', description: '1 to 24 explicit segment lengths, each from 1 to 30 seconds.' },
planOnly: { type: 'boolean', description: 'Price only the planning step.' },
}}
/>

- 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](https://nodaro.ai/docs/nodes/video/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/ with a 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.

### Can I stop a Generate Video Pro run and keep what it made?

Yes. POST /v1/generate-video-pro/:jobId/stop keeps the finished segments, stitches them into the final video and refunds the unused reserve. You can continue the run later as a new job.
