{
    "schema_version": "1.0",
    "id": "page:concepts/async-jobs",
    "kind": "guide",
    "title": "Async jobs",
    "section": "Concepts",
    "description": "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.",
    "canonical_url": "/docs/concepts/async-jobs",
    "markdown_url": "/docs/concepts/async-jobs.md",
    "markdown_sha256": "b5383de5e91bf7962b4dea7f15fee2cd60846989fb450ed1614d08a8335a5712",
    "content_format": "gfm+html",
    "markdown": "# Async jobs\n\nA 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.\n\n## A job's life\n\nEvery 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.\n\n<div class=\"lifecycle\" role=\"img\" aria-label=\"pending, then in_progress, then completed, failed, cancelled or expired\">\n<div class=\"lc-grid\">\n<div class=\"state\"><b>pending</b><span>queued; see queue_position</span></div>\n<div class=\"arrow\"></div>\n<div class=\"state\"><b>in_progress</b><span>rendering; see progress</span></div>\n<div class=\"arrow\"></div>\n<div class=\"lc-ends\">\n<div class=\"state ok\"><b>completed</b><span>outputs ready to download</span></div>\n<div class=\"state bad\"><b>failed</b><span>error says why</span></div>\n<div class=\"state end\"><b>cancelled</b><span>deleted before it finished</span></div>\n<div class=\"state end\"><b>expired</b><span>past expires_at, outputs deleted</span></div>\n</div>\n</div>\n</div>\n\nWhile 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.\n\n> [!NOTE]\n> 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.\n\n## Create a job\n\nThe 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`.\n\n- [`POST /v1/videos`](/docs/reference/create-video.md): Create Video\n- [`POST /v1/music`](/docs/reference/create-music.md): Create Music\n\nThe models that render this way, with their prices:\n\nSee the Video models: /docs/models.md\n\nSee the Music models: /docs/models.md\n\n`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.\n\n## Poll it\n\nFetch 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.\n\n```bash\n# Create the job; it answers 202 with the job at once\nid=$(curl -s https://api.vultrinference.com/v1/videos \\\n  -H \"Authorization: Bearer $VULTR_INFERENCE_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"model\": \"minimax-h3\", \"prompt\": \"A paper boat drifting down a rain-soaked street at dusk\", \"duration\": 4}' | jq -r .id)\n\n# Read it at its polling_url; while it runs, openai-poll-after-ms says how long to wait before reading again\ncurl -i https://api.vultrinference.com/v1/videos/$id \\\n  -H \"Authorization: Bearer $VULTR_INFERENCE_API_KEY\"\n```\n\n```python\nimport os\n\nfrom openai import OpenAI\n\nclient = OpenAI(base_url=\"https://api.vultrinference.com/v1\", api_key=os.environ[\"VULTR_INFERENCE_API_KEY\"])\n\n# Creates the job, then polls at the pace the API asks for (openai-poll-after-ms)\nvideo = client.videos.create_and_poll(\n    model=\"minimax-h3\",\n    prompt=\"A paper boat drifting down a rain-soaked street at dusk\",\n    seconds=\"4\",\n)\nprint(video.status)  # completed, or failed with video.error\n```\n\n```typescript\nimport OpenAI from \"openai\";\n\nconst client = new OpenAI({ baseURL: \"https://api.vultrinference.com/v1\", apiKey: process.env.VULTR_INFERENCE_API_KEY });\n\nlet video = await client.videos.create({\n  model: \"minimax-h3\",\n  prompt: \"A paper boat drifting down a rain-soaked street at dusk\",\n  seconds: \"4\",\n});\n\n// Poll at the pace the API asks for: openai-poll-after-ms, on every answer while the job runs\nfor (let wait = 5000; video.status === \"queued\" || video.status === \"in_progress\"; ) {\n  await new Promise((resolve) => setTimeout(resolve, wait));\n  const { data, response } = await client.videos.retrieve(video.id).withResponse();\n  video = data;\n  wait = Number(response.headers.get(\"openai-poll-after-ms\") ?? wait);\n}\nconsole.log(video.status); // completed, or failed with video.error\n```\n\n## Hear a song as it renders\n\nA 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.\n\nThe 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`.\n\n## Or get a webhook\n\nSend `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.\n\n## Cancel or download\n\n`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.\n\nA `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`.\n",
    "headings": [
        {
            "level": 2,
            "id": "a-jobs-life",
            "text": "A job's life"
        },
        {
            "level": 2,
            "id": "create-a-job",
            "text": "Create a job"
        },
        {
            "level": 2,
            "id": "poll-it",
            "text": "Poll it"
        },
        {
            "level": 2,
            "id": "hear-a-song-as-it-renders",
            "text": "Hear a song as it renders"
        },
        {
            "level": 2,
            "id": "or-get-a-webhook",
            "text": "Or get a webhook"
        },
        {
            "level": 2,
            "id": "cancel-or-download",
            "text": "Cancel or download"
        }
    ],
    "dependencies": {
        "operations": [
            "create-video",
            "create-music"
        ],
        "model_types": [
            "video",
            "music"
        ],
        "examples": [
            "example:guide/video-create-and-poll"
        ],
        "schemas": []
    }
}
