# Jobs

A **job** is one request to a ready program that returns one result: an image or a video clip. You send the program, the route and the input; GPU Forge finds a ready machine of that program (waking or renting one if needed), runs the request and keeps the answer for you.

You pay only for the seconds the result took to render. There are no machines to start, watch or stop.

| Endpoint | Purpose |
|---|---|
| `GET /v1/jobs/quote` | The price of a job before it runs |
| `POST /v1/jobs` | Run a job |
| `GET /v1/jobs/{job_id}` | One job, with its result; can wait until it is done |
| `GET /v1/jobs` | Your recent jobs, without results |

All four need an API key. Programs and their job routes are listed by `GET /v1/catalog` (see [Ready programs](programs.md)).

## Pricing

```
price = render seconds × the program's hourly price ÷ 3600     (minimum $0.002)
```

- **Hourly price**: the program's usual machine price, shown as `avg_price_per_hour` in the catalog.
- **Render seconds**: the render time the program reports for this result. If the program reports none, the time the request took. Never more than the request took.
- **Not billed**: the wait for a machine to wake or start, the time a job spends queued, and any job that fails.
- The charge is taken from your balance when the job finishes and appears in your usage as `job:<job_id>`.
- Your balance must cover the quotes of all your open jobs (queued and running) plus the new one, when you send it and again when it starts. A queued job that your balance no longer covers when its turn comes ends `failed`, free.

### The quote

The quote is the same formula applied to the median render time of the program's recent jobs on that route (10 seconds when there is no history yet). The final price is computed from the actual render seconds, so it can be a little above or below the quote.

`max_price_usd` is compared with the **quote** before the job is accepted. A larger output (more pixels, a longer clip) renders longer and costs more than the median; set your cap with that in mind.

## Get a quote

```
GET /v1/jobs/quote?program={program_id}&route={route}
```

| Query parameter | Required | Description |
|---|---|---|
| `program` | yes | A program id (`capability_id`) from `GET /v1/catalog` |
| `route` | yes | One of the program's `job_routes`, for example `/v1/generate` |

```bash
curl -s "https://api.gpuforge.io/v1/jobs/quote?program=$PROGRAM&route=/v1/generate" \
  -H "Authorization: Bearer $GF_KEY"
```

```json
{"program": "<program id>", "route": "/v1/generate", "quote_usd": 0.002}
```

## Run a job

```
POST /v1/jobs
```

### Request body

| Field | Type | Required | Description |
|---|---|---|---|
| `program` | string | yes | A program id from `GET /v1/catalog` |
| `route` | string | yes | One of that program's `job_routes` |
| `input` | object | no | The JSON body the program's route takes. See [Ready programs](programs.md). Default `{}` |
| `max_price_usd` | number > 0 | no | Refuse the job if its quote is above this. Nothing runs and nothing is charged |

```bash
curl -s "https://api.gpuforge.io/v1/jobs" \
  -H "Authorization: Bearer $GF_KEY" -H "Content-Type: application/json" \
  -d '{
    "program": "<program id>",
    "route": "/v1/generate",
    "input": {"prompt": "Studio product photograph of a ceramic coffee mug on a walnut table, soft window light from the left.", "seed": 11},
    "max_price_usd": 0.02
  }'
```

### Response: `202 Accepted`

```json
{
  "job_id": "job_8c2e41a09b7f",
  "status": "queued",
  "quote_usd": 0.002,
  "next": "GET /v1/jobs/job_8c2e41a09b7f?wait=60 answers when it is done"
}
```

The job runs in the background. Read it with `GET /v1/jobs/{job_id}`.

## Get a job

```
GET /v1/jobs/{job_id}?wait={seconds}
```

| Query parameter | Default | Description |
|---|---|---|
| `wait` | `0` | Hold the request open up to this many seconds (maximum 120) and answer as soon as the job is `done` or `failed`. If the time runs out first, the answer is the job's current state; call again. |

```bash
curl -s "https://api.gpuforge.io/v1/jobs/job_8c2e41a09b7f?wait=60" -H "Authorization: Bearer $GF_KEY"
```

```json
{
  "job_id": "job_8c2e41a09b7f",
  "user_id": "usr_...",
  "program": "<program id>",
  "route": "/v1/generate",
  "status": "done",
  "quote_usd": 0.002,
  "max_price_usd": 0.02,
  "created_at": "2026-11-02T10:15:02.114Z",
  "started_at": "2026-11-02T10:15:03.020Z",
  "finished_at": "2026-11-02T10:15:06.410Z",
  "run_seconds": 2.9,
  "price_usd": 0.002,
  "status_code": 200,
  "error": null,
  "result": {"image": "<base64 JPEG>", "mime_type": "image/jpeg", "width": 832, "height": 1216, "seed": 11, "seconds": {"total": 2.9}}
}
```

### Job fields

| Field | Description |
|---|---|
| `job_id` | The job's id (`job_...`) |
| `user_id` | Your account id |
| `program`, `route` | What was run |
| `status` | `queued`, `running`, `done` or `failed` |
| `quote_usd` | The quote when the job was accepted |
| `max_price_usd` | Your cap, if you sent one |
| `created_at` | When you sent it |
| `started_at` | When a machine began the request (null while queued) |
| `finished_at` | When it finished (null until then) |
| `run_seconds` | The seconds billed |
| `price_usd` | What you were charged. `0` for a failed job |
| `status_code` | The HTTP status the program answered with |
| `error` | Why the job failed, or null |
| `result` | The program's JSON answer (see [Ready programs](programs.md)). On a failed job this holds the program's error body, if it sent one. `null` after 24 hours |

### Statuses

| Status | Meaning |
|---|---|
| `queued` | Waiting for a free ready machine of the program. If none is running, GPU Forge wakes a parked one or rents one; that can take from seconds to several minutes. Not billed. |
| `running` | A machine is working on it. |
| `done` | The program answered with success. `result` holds the answer and `price_usd` the charge. |
| `failed` | The program answered with an error, it did not answer, no machine could be started within 20 minutes, the job was not finished 30 minutes after you sent it, or your balance no longer covered it when it was due to start. Not charged. Read `error` and `result`. |

If a machine stops answering while it runs your job, the job goes back to `queued` and runs again, free.

## List your jobs

```
GET /v1/jobs?limit={n}
```

| Query parameter | Default | Description |
|---|---|---|
| `limit` | `50` | 1–200, newest first |

Returns `{"jobs": [...]}` with the same fields as above, **without** `result`.

## How long results are kept

A job's `input` and `result` are deleted **24 hours** after it finishes. The rest of the record (program, route, times, seconds, price) stays in your history. Download results you want to keep as soon as they are done.

## Timing

- A job on a warm machine starts at once. Image jobs usually take a few seconds; video jobs from tens of seconds to a few minutes, depending on length and quality.
- The first job after a quiet period may wait while a machine wakes (usually under a minute) or a new one is rented and prepared (several minutes, sometimes up to 15–20).
- A job that has not reached a machine within **20 minutes** fails, free of charge. A single request may run for up to **10 minutes**, and a job not finished **30 minutes** after you sent it fails, free.
- A job is sent to the program once. If the program does not answer (for example it takes longer than 10 minutes), the job fails, free; send it again if you want another try. Only a machine that could not be reached at all gets the job again automatically.
- Jobs of one program run in order of arrival as machines become free. When the queue grows, GPU Forge starts more machines of that program, up to a limit per program.

## Errors

| Status | When | What to do |
|---|---|---|
| `402` | The quote is above your `max_price_usd`. The body includes `quote_usd` and `max_price_usd`. | Raise the cap or change the input. |
| `402` | Your balance does not cover this job on top of your other open jobs (the message gives the numbers). | Top up, or wait for open jobs to finish. See [Billing](billing.md). |
| `404` | The `program` is not in the catalog, or the job does not exist or is not yours. | Check the id with `GET /v1/catalog` or `GET /v1/jobs`. |
| `422` | The `route` is not one of the program's `job_routes` (the message lists them), or the body is invalid. | Use a listed route; fix the field named in the error. |

A program's own refusal (for example an invalid field or a refused prompt) does not fail the `POST`: the job ends `failed`, free, with the program's answer in `result` and its status in `status_code`. See [Errors](errors.md).

## Example: a short video clip

```bash
curl -s "https://api.gpuforge.io/v1/jobs" \
  -H "Authorization: Bearer $GF_KEY" -H "Content-Type: application/json" \
  -d '{
    "program": "<LTX-2.5 program id>",
    "route": "/v1/jobs/text-to-video",
    "input": {
      "prompt": "A slow aerial shot over a rocky coastline at dusk. Waves break against the cliffs. The camera glides forward. The sound of wind and surf.",
      "seconds": 5, "format": "16:9"
    }
  }'
```

When it is done, `result.video` is the MP4 file in base64:

```bash
curl -s "https://api.gpuforge.io/v1/jobs/<job_id>?wait=120" -H "Authorization: Bearer $GF_KEY" \
  | python3 -c "import sys,json,base64; j=json.load(sys.stdin); \
open('coast.mp4','wb').write(base64.b64decode(j['result']['video'])) if j['status']=='done' else print(j['status'], j.get('error'))"
```
