Skip to content

DocsAPI reference

Async jobs

The job object, status lifecycle, polling with backoff, listing and filtering, cancellation, 24-hour output retention, and idempotent submits.


INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See Early access.

Work that takes too long for a single HTTP request returns a job instead of a result. Today only video generation creates jobs; images and audio are synchronous and return inline. Any future async endpoint uses the same job API.

A job is created with 202 Accepted, moves through queued and running, and settles in exactly one terminal state. Terminal states do not change, and output files are kept for 24 hours. A key reads only jobs inside its own scope; an unknown job ID, or one outside that scope, answers 400 invalid_request_error.

Endpoints
GET  https://api.infro.io/v1/jobs/{id}
GET  https://api.infro.io/v1/jobs
POST https://api.infro.io/v1/jobs/{id}/cancel

The job object

json
{
  "id": "job_7d41ba90",
  "object": "job",
  "type": "video.generation",
  "status": "succeeded",
  "model": "kuaishou/kling-o3",
  "route": "primary",
  "provider": "openai-compatible",
  "connection": "conn_8b21c4e0",
  "progress": 1,
  "created_at": 1755950000,
  "started_at": 1755950006,
  "completed_at": 1755950139,
  "expires_at": 1756036539,
  "output": {
    "url": "https://api.infro.io/media/vid/2026-09-24/org_.../job_7d41ba90.mp4?exp=1756036539&sig=...",
    "duration_seconds": 5,
    "resolution": "720p",
    "aspect_ratio": "16:9",
    "mime_type": "video/mp4"
  },
  "usage": {
    "seconds": 5,
    "cost": 1.25,
    "price_source": "list"
  },
  "error": null
}
idstring
Opaque identifier prefixed job_. Stable for the life of the job, including when the submit fails over to another target on your route.
objectstring
Always job.
typestring
What produced the job, e.g. video.generation. Treat it as an open set — new async endpoints add new types.
statusstring
queued, running, succeeded, failed, or canceled. The last three are terminal.
modelstring
The model that served the job. When you requested a route, this is the target model that ran.
routestring | null
Which position in the model's chain ran the work — primary, or standby_a and so on. Null while queued, because nothing has been dispatched yet.
provider / connectionstring | null
The provider kind and the ID of your connection that ran the render. Null while queued.
progressnumber
Best-effort 0–1 completion estimate, updated each time INFRO checks the provider (every one to five minutes) and 1 on success. Providers that report nothing leave it at 0, so never build UI that requires it to move.
created_at / started_at / completed_atinteger | null
Unix seconds. started_at is set when a provider accepts the submit, completed_at when the job becomes terminal (including a cancel); both are null until then.
expires_atinteger | null
Unix seconds at which the output link stops working — 24 hours after completion. Null until the job succeeds, and null again once the output has been deleted.
outputobject | null
Result payload, present only on succeeded. Shape depends on type; for video.generation it is {url, duration_seconds, resolution, aspect_ratio, mime_type}.
usageobject | null
Metered units plus cost, an estimate of what your provider will bill. {seconds, cost, price_source} for video; cost is null when the model is unpriced. Present only on succeeded — INFRO records no cost for failed and canceled jobs.
errorobject | null
{message, type, code} on failed, using the same shape and codes as synchronous errors. Null otherwise.

Status lifecycle

  1. queued — the request was accepted and validated. The render is being submitted to one of your connections, usually within seconds.
  2. running — a provider accepted the render; route, provider, connection, and started_at are set. Failover happens only while submitting: a running render is never moved to another connection.
  3. succeeded — terminal. output, usage, and completed_at are populated, and expires_at starts the 24-hour clock on the output file.
  4. failed — terminal. No target on the route accepted the submit, the provider reported the render failed, or it was still unfinished after 60 status checks (408 request_timeout); error says why. INFRO records no cost.
  5. canceled — terminal, and only reachable from queued or running via the cancel endpoint. INFRO stops tracking the render and records no cost; it does not currently cancel the render at your provider.

Retrieving a job

curl https://api.infro.io/v1/jobs/job_7d41ba90 \
  -H "Authorization: Bearer $INFRO_API_KEY"

Polling with backoff

Polling is the fallback for clients that cannot receive a callback. Four rules keep it cheap and well-behaved:

  • Start around 2 seconds, multiply the delay by 1.5 after each check, and cap it at 30 seconds. Jobs rarely finish faster than the first interval, so a tighter loop buys nothing.
  • Add a little jitter when many workers poll at once, so retries do not synchronize into bursts.
  • Set your own deadline and give up on it. A job with no terminal state after your timeout is a job to surface to the user, not to poll forever.
  • Poll from a worker or a background isolate, never from inside a request handler that a user is waiting on.

A complete submit-then-poll implementation is in Video generation. Poll responses are cheap but not free — they count against your rate limits.

If your code runs anywhere that can accept an inbound HTTPS request, use webhooks instead. One signed delivery replaces the entire polling loop.

Listing and filtering

bash
curl "https://api.infro.io/v1/jobs?status=running&limit=20" \
  -H "Authorization: Bearer $INFRO_API_KEY"
statusstring
Filter to one status. Repeat the parameter to match several, e.g. ?status=queued&status=running for everything still in flight.
typestring
Filter by job type, e.g. video.generation.
limitinteger
Page size, 1–100. Defaults to 20.
afterstring
Cursor for the next page: pass the next_cursor from the previous response.
json
{
  "object": "list",
  "data": [
    {
      "id": "job_9a02f7ae",
      "object": "job",
      "type": "video.generation",
      "status": "running",
      "model": "runway/gen-4",
      "route": "primary",
      "provider": "openai-compatible",
      "connection": "conn_8b21c4e0",
      "progress": 0.4,
      "created_at": 1755950320
    }
  ],
  "has_more": true,
  "next_cursor": "job_9a02f7ae"
}

Jobs come back newest first, each as the full job object. Walk pages by passing next_cursor as after until has_more is false. An unknown status value is refused with 400 invalid_request_error. Listing is the quickest way to find work orphaned by a crashed worker: filter on queued and running at startup and re-attach.

Cancellation

bash
curl -X POST https://api.infro.io/v1/jobs/job_7d41ba90/cancel \
  -H "Authorization: Bearer $INFRO_API_KEY"

Cancel returns the updated job. It is valid only from queued or running; calling it on a terminal job returns 400 invalid_request_error. Cancellation races with completion — if the render finished a moment before your call, you get the succeeded job back with its output and cost estimate. Cancel does not currently stop the render at your provider, and an unknown job ID also answers 400 invalid_request_error. Branch on the status in the response rather than assuming the cancel took effect.

Retention

Output files live for 24 hours after completion, at expires_at. After that the URL returns 404 and INFRO deletes its copy of the file; the job record itself stays readable with output and expires_at set to null, so you keep the record of what ran, on which provider, at what estimated cost.

Add download=1 to any output link — a job's output.url, an image's data[].url, or a stored speech file's audio.url — to receive the same file as an attachment (Content-Disposition: attachment), named after the request or job ID, so a browser saves it instead of displaying it. The signature still applies and the parameter does not extend the expiry.

Treat the output URL as a handoff, not as storage. Download the file — or copy it straight to your own bucket — as soon as the job succeeds. Regenerating an expired render is billed again by your provider and will not be identical unless you pinned a seed.

Idempotency

Submitting async work twice is expensive, and a retried POST after a timeout is exactly the situation where it happens. Send an Idempotency-Key header on POST /v1/videos: any unique string up to 255 characters, a UUID being the obvious choice.

bash
curl https://api.infro.io/v1/videos \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1b9d0e-2c19-4a3a-9d2a-8fd6b3b2f7c1" \
  -d '{
    "model": "kuaishou/kling-o3",
    "prompt": "Slow dolly over a foggy pine ridge at dawn",
    "duration_seconds": 5
  }'
  • INFRO stores the first 202 response for a key and replays it for 24 hours, so a retry returns that original response — the job as it was when created, still queued — instead of starting a second render. Read the job's current state with GET /v1/jobs/{id}.
  • Reusing a key with a different request body returns 400 invalid_request_error with code idempotency_key_reuse — the key identifies one specific submission, not a slot. Retrying while the first request is still being processed returns 400 with code idempotency_key_in_progress; retry shortly.
  • Keys are scoped to your organization, so two services can safely derive keys from their own record IDs as long as those IDs do not collide.
  • Only POST /v1/videos honors the header. Every other endpoint ignores it; GET and cancel are already safe to retry.
↑ ↓ navigate↵ open/ anywhere in docs