OP Inference

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.

ModalityRoute
ImagePOST /v1/images/generations
VideoPOST /v1/videos
SpeechPOST /v1/audio/speech
TranscriptionPOST /v1/audio/transcriptions
MusicPOST /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.

stateMeaning
queuedAccepted, not started
processingRunning. progress.percent and progress.phase report how far, when the model reports it
succeededDone. output holds the files
failedEnded without output. error says why. A failed job is not charged
canceledCanceled 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"}
  }
}
  • url is a signed link. Download it before url_expires_at. A later GET /v1/jobs/{id} returns a fresh link while the file is still kept.
  • checksum_sha256 lets you check the download.
  • purged_at appears once retention has removed the file. See Data retention.
  • usage.units counts what was billed, and usage.cost.billing_usd is 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.