How media jobs work
Submit a job, poll it, fetch the output.
Image, video, speech, transcription and music requests are jobs. Every media route answers
202 Accepted with a job resource, even when the model finishes quickly. You then poll the job,
or receive a webhook, and download the output from a signed link.
| Modality | Route |
|---|---|
| Image | POST /v1/images/generations |
| Video | POST /v1/videos |
| Speech | POST /v1/audio/speech |
| Transcription | POST /v1/audio/transcriptions |
| Music | POST /v1/audio/music |
These routes take JSON and answer with a job, not the OpenAI response shape. An OpenAI SDK's
images.generate or audio.speech.create will not parse the reply. Call them over plain HTTP.
1. Submit
The body is model plus the model's own inputs. Each model's page at
opinference.com/models lists the inputs it accepts, which are
required, and their ranges. An input the model does not accept, or a value out of range, is
refused with 400 invalid_request naming the field in param.
curl https://api.opinference.com/v1/images/generations \
-H "Authorization: Bearer $OP_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"model": "black-forest-labs/flux-1-schnell",
"prompt": "A lighthouse at dawn, watercolor.",
"width": 1024,
"height": 1024
}'{
"id": "job_...",
"generation_id": "gen_...",
"object": "job",
"state": "queued",
"modality": "image",
"model": "black-forest-labs/flux-1-schnell",
"created_at": "2026-10-02T11:29:21Z",
"progress": {"percent": null, "phase": "queued", "queue_position": null},
"op_metadata": {
"generation_id": "gen_...",
"requested_model": "black-forest-labs/flux-1-schnell",
"retention_class": "standard",
"region": "global",
"policy": []
}
}A 202 means the job is stored and will run. Send an Idempotency-Key, one per job, and reuse
the same value if you retry the submit: the retry returns the same job instead of starting a
second one. See Idempotency.
2. Poll
GET /v1/jobs/{id} returns the job's current state.
state | Meaning |
|---|---|
queued | Accepted, not started |
processing | Running. progress.percent and progress.phase report how far, when the model reports it |
succeeded | Done. output holds the files |
failed | Ended without output. error says why. A failed job is not charged |
canceled | Canceled before it finished |
succeeded, failed and canceled are final. Poll every few seconds until you see one of them.
An id that does not exist, or belongs to another account, returns 404 job_not_found.
3. Fetch the output
{
"id": "job_...",
"state": "succeeded",
"completed_at": "2026-10-02T11:29:24Z",
"progress": {"percent": 100, "phase": "done", "queue_position": null},
"output": [
{
"url": "https://...",
"url_expires_at": "2026-10-02T12:29:30Z",
"type": "image/jpeg",
"bytes": 432629,
"checksum_sha256": "e333f62a..."
}
],
"usage": {
"units": {"images": 1},
"cost": {"billing_usd": 0.000627, "price_version": "2026-09-28"}
}
}urlis a signed link. Download it beforeurl_expires_at. A laterGET /v1/jobs/{id}returns a fresh link while the file is still kept.checksum_sha256lets you check the download.purged_atappears once retention has removed the file. See Data retention.usage.unitscounts what was billed, andusage.cost.billing_usdis its price.
The whole loop
import os, time, urllib.request, hashlib
import requests
API = "https://api.opinference.com/v1"
headers = {"Authorization": f"Bearer {os.environ['OP_API_KEY']}"}
job = requests.post(f"{API}/images/generations", headers=headers, json={
"model": "black-forest-labs/flux-1-schnell",
"prompt": "A lighthouse at dawn, watercolor.",
"width": 1024,
"height": 1024,
}).json()
while job["state"] not in ("succeeded", "failed", "canceled"):
time.sleep(2)
job = requests.get(f"{API}/jobs/{job['id']}", headers=headers).json()
if job["state"] != "succeeded":
raise SystemExit(f"job {job['id']} {job['state']}: {job.get('error')}")
out = job["output"][0]
data = urllib.request.urlopen(out["url"]).read()
assert hashlib.sha256(data).hexdigest() == out["checksum_sha256"]
open("lighthouse.jpg", "wb").write(data)
print("saved", out["bytes"], "bytes")Cancel
POST /v1/jobs/{id}/cancel asks for a job to stop. It answers 202 with the job. A job already
in a final state answers 409 with a message naming its state. A job canceled before it reached
the model is not charged. One canceled after the model started can be charged for the work the
model reported; the job's usage.cost shows it.
Retry a failed job
Submit the same body again, with retry_of set to the failed job's id. The new job links back to
the old one in its own retry_of.