---
title: "Jobs"
description: "One envelope and one read model for every kind of work that roasOS does."
url: https://docs.roasos.com/concepts/jobs
---

# Jobs

Each model endpoint starts a **job**. An image, a scene, an avatar, a video, a UGC clip, audio and lipsync are all jobs. They have the same envelope and the same read endpoints.

An analysis, an agent run and a research job are different. Each one has its own envelope and its own read path. Refer to [Work that is not a job](#work-that-is-not-a-job).

## The envelope

A create request returns `201` with the job:

```json
{
  "data": {
    "id": "6f31c07d-...",
    "kind": "video",
    "model": "kling-4",
    "status": "queued",
    "credits_reserved": 9,
    "created_at": "2026-08-19T10:04:11Z",
    "links": {
      "self": "/v1/jobs/6f31c07d-...",
      "status": "/v1/jobs/6f31c07d-.../status",
      "result": "/v1/jobs/6f31c07d-.../result",
      "events": "/v1/jobs/6f31c07d-.../events"
    }
  }
}
```

The `id` is a UUID. Every public identifier of roasOS uses that one format: a job, a quote, an analysis, an agent run, a research job. An id in another format is a `400 invalid_resource_id`.

The reserved credits are in `credits_reserved`. Read the four `links` values, and do not build a path of your own.

## Status

A job has one of five statuses:

```
queued -> processing -> completed | failed | canceled
```

`completed`, `failed` and `canceled` are terminal. A job does not change after it is terminal.

## The kinds that `GET /v1/jobs` reads

Seven kinds appear in the list: `image`, `scene`, `avatar`, `video`, `ugc`, `audio` and `lipsync`.

Each family reports the kind of the media that it gives you. An image edit, a product shot and an Ideogram reframe are all `image`. A dub, a caption, a video reframe and an assembly operation are all `video`. Music is `audio`. So a filter of `kind=assembly` does not exist, and `kind=image` finds the product shot.

The filter also accepts `voice`, `batch` and `research`. Each of those three answers an empty page, because each one has its own resource:

* A voice is at `GET /v1/voices`.
* A batch item appears in the list under the kind of its media, and never under `batch`.
* A research job is at `GET /v1/research/jobs/{id}`.

## Read a job

| Method and path            | Gives you                                                                       |
| -------------------------- | ------------------------------------------------------------------------------- |
| `GET /v1/jobs`             | a list of jobs; filter with `kind`, `status`, `cursor` and `limit`              |
| `GET /v1/jobs/{id}`        | the envelope, plus `result` when the job is terminal and `error` when it failed |
| `GET /v1/jobs/{id}/status` | `{ id, status, updated_at }` — the light poll                                   |
| `GET /v1/jobs/wait?ids=…`  | the light poll of 1 to 12 jobs, after they finish or after the timeout          |
| `GET /v1/jobs/{id}/result` | the result object                                                               |
| `GET /v1/jobs/{id}/events` | the steps of the run, in order                                                  |

The list is paged. Refer to [Pagination](/concepts/pagination).

## Poll for the result

Poll `GET /v1/jobs/{id}/status` every few seconds. When the status is `completed`, read `GET /v1/jobs/{id}/result`.

```bash
curl https://api.roasos.com/v1/jobs/6f31c07d-.../status \
  -H "Authorization: Bearer roasos_sk_..."
```

<Callout type="warn">
  `GET /v1/jobs/{id}/result` gives `409 run_not_completed` while the job is not terminal. This is not an error in your code. Poll the status endpoint first.
</Callout>

## Wait for many jobs

A video job can run for some minutes. To wait for up to 12 jobs in one call, use `GET /v1/jobs/wait`.

```bash
curl "https://api.roasos.com/v1/jobs/wait?ids=6f31c07d-...,9a2b44e0-...&timeout_seconds=50" \
  -H "Authorization: Bearer roasos_sk_..."
```

* `ids` — 1 to 12 different job IDs, separated by commas.
* `timeout_seconds` — 1 to 55. The default is `50`.

roasOS reads the jobs every 2 to 5 seconds. The call returns when all the jobs are `completed`, `failed` or `canceled`, or before `timeout_seconds` is over.

```json
{ "data": {
  "jobs": [
    { "id": "6f31c07d-...", "status": "completed", "updated_at": "2026-09-28T10:02:11Z" },
    { "id": "9a2b44e0-...", "status": "processing", "updated_at": "2026-09-28T10:00:03Z" }
  ],
  "all_terminal": false,
  "waited_ms": 49250 } }
```

* Each item in `jobs` is the object that `GET /v1/jobs/{id}/status` gives. The items are in the order of `ids`.
* A job ID that is not in your workspace gives `{ "id": "…", "error": { "code": "job_not_found", "message": "Job not found" } }` in its place. roasOS does not wait for it.
* When `all_terminal` is `false`, send the call again with the IDs that are not finished.

The call is free. A bad `ids` or `timeout_seconds` value gives `400 validation_error`.

## Wait inside the create call

To wait inside the create call, send `wait_timeout_seconds` (0 to 60). roasOS then waits up to that time and returns the terminal job if the run finishes. The default is `0`.

## Events

`GET /v1/jobs/{id}/events` gives the steps of the run, in order: `queued`, `provider_submitted`, `provider_completed`, `settled`, `failed` and `canceled`. A delivered webhook adds `webhook_delivered` or `webhook_failed`.

<Callout>
  roasOS records these events while the job runs. A job that ran before the timeline existed has no recorded events. For one of those, roasOS calculates the steps from the times on the job, and it shows no webhook event. Use the events to see how long each step took. Do not use them as a live stream.
</Callout>

## Cancel a job

`POST /v1/jobs/{id}/cancel` records a cancellation on the job. It answers `409 job_not_cancelable` when the job is already terminal.

The response carries `cancel_honoured`.

* `true` — the job had not reached the model provider. roasOS stops the run, refunds the whole reservation, and the job becomes `canceled`.
* `false` — the provider already holds the request. roasOS records the cancellation, but the run finishes and you are billed for it. A provider charges for work that it has started.

A cancel is therefore free only before the provider submit, which is usually the first few seconds of a job.

## Failure and refunds

If the status is `failed`, the job carries an `error` object with a stable `code`. roasOS releases the credit reservation. It does not charge you for a job that failed. Refer to [Errors](/concepts/errors) and to [Quotes and credits](/concepts/quotes-and-credits).

## Results are files

A result is a set of immutable objects on `https://cdn.roasos.com`. The URL does not change, and the bytes do not change. You can therefore send a result URL as the media input of your next request. Refer to [Reference media and uploads](/concepts/reference-media).

## Work that is not a job

Three kinds of work do not use the job envelope. Read each one at its own path.

| Work           | Started by                                                                                                          | Read at                           |
| -------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| An analysis    | `POST /v1/workflows/analyze`, `POST /v1/workflows/extract-brand`                                                    | `GET /v1/workflows/analyses/{id}` |
| An agent run   | `POST /v1/agent/completions`, `POST /v1/skills/{id}/runs`, `POST /v1/workflows/remix`, `POST /v1/workflows/statics` | `GET /v1/agent-runs/{id}`         |
| A research job | `POST /v1/research/jobs`                                                                                            | `GET /v1/research/jobs/{id}`      |

Each of the three publishes its own status words, and the words are not the five above. Read them on the page of that family: [Analyze an ad](/workflows/analyze-an-ad), [Agent runs](/agent/runs) and [Research ads](/workflows/research-ads).

An agent run and an analysis start jobs of their own while they work. Those jobs are ordinary jobs, and you read them here.

## The older paths

`GET /v1/generations` and `GET /v1/generations/{id}` still read image, video, audio and lipsync jobs. They are the older shape of the same data. Use `/v1/jobs` for new work: it reads every kind, and it has the status, result and events endpoints.

Two create paths are deprecated: `POST /v1/generations` and `POST /v1/scenes`. Each one answers with three headers.

| Header        | Value                                    |
| ------------- | ---------------------------------------- |
| `Deprecation` | `true`                                   |
| `Sunset`      | `Mon, 30 Nov 2026 00:00:00 GMT`          |
| `Link`        | `</v1/catalog>; rel="successor-version"` |

The `Sunset` date is the date after which the operation can stop answering. Read `GET /v1/catalog` to find the model endpoint that replaces your call. Move to it before that date.
