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.
GET https://api.infro.io/v1/jobs/{id}
GET https://api.infro.io/v1/jobs
POST https://api.infro.io/v1/jobs/{id}/cancelThe job object
{
"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. statusstringqueued,running,succeeded,failed, orcanceled. The last three are terminal.modelstring- The model that served the job. Differs from the model you requested if a
fallbacksentry was used. providerstring | null- Bare lowercase slug of the provider that ran the work. Null while
queued. progressnumber- Best-effort
0–1completion estimate whilerunning. Providers that report nothing leave it at0, so never build UI that requires it to move. created_at / started_at / completed_atinteger | null- Unix seconds.
started_atis set when a provider accepts the work,completed_atwhen the job becomes terminal; both are null until then. expires_atinteger | null- Unix seconds at which
outputis deleted — 24 hours after completion. Null until the job succeeds. outputobject | null- Result payload, present only on
succeeded. Shape depends ontype; forvideo.generationit is{url, duration_seconds, resolution, aspect_ratio, mime_type}. usageobject | null- Metered units plus
cost, the exact USD charged. Present only onsucceeded— failed and canceled jobs are not billed. errorobject | null{message, type, code}onfailed, using the same shape and codes as synchronous errors. Null otherwise.
Status lifecycle
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.running— a provider accepted the work;providerandstarted_atare set. Failover has already happened by now if it was going to: once output starts coming back, INFRO cannot re-route.succeeded— terminal.output,usage, andcompleted_atare populated, andexpires_atstarts the 24-hour clock on the output file.failed— terminal. Every provider for the model and every entry infallbackswas exhausted;errorsays why. Nothing is billed.canceled— terminal, and only reachable fromqueuedorrunningvia 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
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=runningfor everything still in flight. typestring- Filter by job type, e.g.
video.generation. limitinteger- Page size,
1–100. Defaults to20. afterstring- Cursor for the next page: pass the
next_cursorfrom the previous response.
{
"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
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.
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.
GETand cancel are already idempotent and ignore it.