# Async jobs

A video or a song takes seconds to minutes to render, so creating one answers 202 at once with a job. Poll the job, listen to a song as it renders, or have a webhook tell you when it is done.

## A job's life

Every job moves through the same states, on `/v1/videos` and `/v1/music` alike. The last four are final: a job in one of them never changes again.

<div class="lifecycle" role="img" aria-label="pending, then in_progress, then completed, failed, cancelled or expired">
<div class="lc-grid">
<div class="state"><b>pending</b><span>queued; see queue_position</span></div>
<div class="arrow"></div>
<div class="state"><b>in_progress</b><span>rendering; see progress</span></div>
<div class="arrow"></div>
<div class="lc-ends">
<div class="state ok"><b>completed</b><span>outputs ready to download</span></div>
<div class="state bad"><b>failed</b><span>error says why</span></div>
<div class="state end"><b>cancelled</b><span>deleted before it finished</span></div>
<div class="state end"><b>expired</b><span>past expires_at, outputs deleted</span></div>
</div>
</div>
</div>

While a job waits, `queue_position` counts the jobs ahead of it for the same model; while it renders, `progress` estimates how far along it is, from 0 to 100. A render that fails before it starts, because the model was busy or out of reach, goes back to `pending` and is tried again, up to three times; a refusal from the model fails the job with its message.

> [!NOTE]
> On `/v1/videos`, OpenAI's SDKs see `queued` for `pending`, and read `cancelled` and `expired` as `failed` with the reason as the error's code, since their `videos.poll()` knows no other states.

## Create a job

The create call checks the request before it answers: a malformed field, or a control the model does not have, is a 400 right away, never a job that fails later. Any other field the model does not read is listed in the job's `warnings`. It answers 202 with the job and its `polling_url`.

- [`POST /v1/videos`](/docs/reference/create-video.md): Create Video
- [`POST /v1/music`](/docs/reference/create-music.md): Create Music

The models that render this way, with their prices:

See the Video models: /docs/models.md

See the Music models: /docs/models.md

`POST /v1/music` can also wait for the song: send `Prefer: wait=30` (at most 60 seconds) and it answers 200 with the finished job if it ends in time, or 202 with the job as it stands.

## Poll it

Fetch the job at its `polling_url` until it reaches a final state. While it is still going, every answer carries `openai-poll-after-ms`: how long to wait before asking again, worked out from the job's place in the queue and its progress, between 2 and 10 seconds. OpenAI's SDKs read it for you.

```bash
# Create the job; it answers 202 with the job at once
id=$(curl -s https://api.vultrinference.com/v1/videos \
  -H "Authorization: Bearer $VULTR_INFERENCE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "minimax-h3", "prompt": "A paper boat drifting down a rain-soaked street at dusk", "duration": 4}' | jq -r .id)

# Read it at its polling_url; while it runs, openai-poll-after-ms says how long to wait before reading again
curl -i https://api.vultrinference.com/v1/videos/$id \
  -H "Authorization: Bearer $VULTR_INFERENCE_API_KEY"
```

```python
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.vultrinference.com/v1", api_key=os.environ["VULTR_INFERENCE_API_KEY"])

# Creates the job, then polls at the pace the API asks for (openai-poll-after-ms)
video = client.videos.create_and_poll(
    model="minimax-h3",
    prompt="A paper boat drifting down a rain-soaked street at dusk",
    seconds="4",
)
print(video.status)  # completed, or failed with video.error
```

```typescript
import OpenAI from "openai";

const client = new OpenAI({ baseURL: "https://api.vultrinference.com/v1", apiKey: process.env.VULTR_INFERENCE_API_KEY });

let video = await client.videos.create({
  model: "minimax-h3",
  prompt: "A paper boat drifting down a rain-soaked street at dusk",
  seconds: "4",
});

// Poll at the pace the API asks for: openai-poll-after-ms, on every answer while the job runs
for (let wait = 5000; video.status === "queued" || video.status === "in_progress"; ) {
  await new Promise((resolve) => setTimeout(resolve, wait));
  const { data, response } = await client.videos.retrieve(video.id).withResponse();
  video = data;
  wait = Number(response.headers.get("openai-poll-after-ms") ?? wait);
}
console.log(video.status); // completed, or failed with video.error
```

## Hear a song as it renders

A song on a model that streams has a `stream_url` while it is `pending` or `in_progress`. `GET /v1/music/{id}/stream` answers `audio/wav` and plays the song from its first byte as it is made, then follows it until it ends. A listener who connects late, even after the song completed, still hears it from the start for 10 minutes; after that the stream answers 409 and the song is at its `content` URL.

The stream ends when the song does but cannot say why, so read the job afterwards: a stream that ends early belongs to a job that `failed` or was `cancelled`.

## Or get a webhook

Send `callback_url` (a public `https` URL) with the create call and it is called when the job reaches a final state, so there is nothing to poll. Each delivery is signed with your webhook secret and carries the job; answer it within 10 seconds or it is sent again. [Webhooks](/docs/concepts/webhooks) has the events, how to verify a delivery, and a receiver to start from.

## Cancel or download

`DELETE` on a job that is still `pending` or `in_progress` cancels it at once, and it is not billed. On a finished job it deletes the job and its outputs.

A `completed` job lists its files in `unsigned_urls`; fetch them with your API key before `expires_at`, after which they are deleted and the job turns `expired`.
