Skip to content
INFRO

Documentation

Async jobs

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


Work that takes too long for a single HTTP request returns a job instead of a result. Today that means video generation and data exports; images and audio are synchronous and return inline. Whatever creates a job, the job API below is identical.

A job is created with 202 Accepted, moves through queued and running, and settles in exactly one terminal state. Terminal jobs are immutable, and their outputs are retained for 24 hours.

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-2.5",
  "provider": "fal",
  "progress": 1,
  "created_at": 1755950000,
  "started_at": 1755950006,
  "completed_at": 1755950139,
  "expires_at": 1756036539,
  "output": {
    "url": "https://cdn.infro.io/vid/7d41ba90.mp4",
    "duration_seconds": 5,
    "resolution": "720p",
    "mime_type": "video/mp4"
  },
  "usage": {
    "seconds": 5,
    "cost": 1.25
  },
  "error": null
}
idstring
Opaque identifier prefixed job_. Stable for the life of the job, including across automatic failover to another provider or fallback model.
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. Differs from the model you requested if a fallbacks entry was used.
providerstring | null
Bare lowercase slug of the provider that ran the work. Null while queued.
progressnumber
Best-effort 01 completion estimate while running. 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 work, completed_at when the job becomes terminal; both are null until then.
expires_atinteger | null
Unix seconds at which output is deleted — 24 hours after completion. Null until the job succeeds.
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, the exact USD charged. Present only on succeeded — failed and canceled jobs are not billed.
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 job is waiting for capacity on a provider that satisfies your routing policy. Nothing is billed yet.
  2. running — a provider accepted the work; provider and started_at are set. Failover has already happened by now if it was going to: once output starts coming back, INFRO cannot re-route.
  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. Every provider for the model and every entry in fallbacks was exhausted; error says why. Nothing is billed.
  5. canceled — terminal, and only reachable from queued or running via the cancel endpoint. In-flight work is stopped and nothing is billed.

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, 1100. 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",
      "provider": "runway",
      "progress": 0.4,
      "created_at": 1755950320
    }
  ],
  "has_more": true,
  "next_cursor": "job_9a02f7ae"
}

Jobs come back newest first. Walk pages by passing next_cursor as after until has_more is false. 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 and it is billed normally. 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 file is deleted and its URL returns 404; the job record itself remains readable for 30 days with output set to null, so you keep the audit trail of what ran, on which provider, at what cost.

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 costs full price 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 the request that creates the job: 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-2.5",
    "prompt": "Slow dolly over a foggy pine ridge at dawn",
    "duration_seconds": 5
  }'
  • INFRO stores the first response for a key and replays it for 24 hours, so a retry returns the original job instead of starting a second render.
  • Reusing a key with a different request body returns 400 invalid_request_error — the key identifies one specific submission, not a slot.
  • Keys are scoped to your account, so two services can safely derive keys from their own record IDs as long as those IDs do not collide.
  • Only creating requests honor the header. GET and cancel are already idempotent and ignore it.