One POST in, every field back out
The request body checked against the schema, what a fallback and a quota carry field by field, the shape a run keeps of itself step by step, and the one error envelope every endpoint shares — chat, media and pipelines alike.
- One error envelope
- A record per step
- Sync · async · stream
- Idempotency-Key
POST /v1/pipelines
{
"name": "Scene → clip → cut with the voiceover",
"definition": {
"inputs": [
{ "name": "brief", "type": "text", "label": "Scene brief" },
{ "name": "voice", "type": "audio", "label": "Voiceover" }
],
"steps": [
{ "id": "scene", "type": "media", "model": "nano-banana-2",
"input": { "prompt": "${input.brief}" } },
{ "id": "clip", "type": "media", "model": "veo-3.1-fast",
"input": { "prompt": "${input.brief}",
"image": "${steps.scene.output.url}" } },
{ "id": "final", "type": "video.add_audio",
"input": { "video_url": "${steps.clip.output.url}",
"audio_url": "${input.voice}" },
"params": { "mode": "replace" } }
],
"output": { "video": "${steps.final.output.url}" }
}
}Routing, fallback, quotas, accounting — by name
Four responsibilities sit between your request and the model. Each one is a specific set of fields, not a promise.
- Routing resolves the slug you sent to exactly one catalogue row — slugs are unique, and there is no alias layer for one to stand in for another. That row is often reachable through more than one upstream; the highest-ranked one takes the request, and the rest become its own failover endpoints.
- Fallback is a list of models scoped to one workspace and one source model — never to the API key that sent the request. The model you asked for goes first; the ones behind it are ordered by how healthy each provider has been and what it costs, so the chain routes around a bad afternoon instead of walking into it. Flagship models arrive with a default chain already configured, so they fail over before you set anything up; a source model outside that set fails over once you give its workspace a chain for it.
- Quotas resolve through layers, narrowest one that sets a field winning: platform default, then plan, then — on plans that carry custom quotas — the key creator’s own preset and the key’s own. The fields a preset carries: allowedModels, maxRequestsPerMinute, maxBudgetCredits with a budgetPeriod of hourly, daily, weekly or monthly, and allowSchedules with minScheduleIntervalMinutes. A daily token ceiling exists as well, but it belongs to the plan (rateLimitTpd), not to anything you set on a key.
- Accounting rolls up requestCount, inputTokens, outputTokens, totalTokens and retailCostUsd per key, per day — the numbers behind the dashboard’s per-key usage view.
A step is a record, not a line of output
stepRuns on a pipeline run is one array, one shape, whether a step ran, was skipped or failed — a client reads one structure instead of branching on what happened.
- status is one of six values. skipped carries its own reason — a step whose condition evaluates false gets skippedReason: "condition_false" and output: null, never dispatched and never billed.
- error, when a step fails, carries the same { code, message } pair the API itself returns on a failed request — one vocabulary for both.
- childRunIds names the runs a foreach iteration or a sub-pipeline spawned, so a nested run stays reachable by id.
- creditsUsed and durationMs are recorded per step, not only for the run as a whole — the two numbers that show which step in a five-step chain is the expensive one.
GET /v1/pipelines/runs/{id}
"stepRuns": [
{
"id": "clip",
"type": "media",
"status": "succeeded",
"output": { "url": "https://cdn.infery.ai/.../clip.mp4" },
"creditsUsed": 4.2,
"durationMs": 8340,
"attempt": 1
},
{
"id": "captions",
"type": "video.add_audio",
"status": "skipped",
"skippedReason": "condition_false",
"output": null,
"creditsUsed": 0,
"durationMs": 0,
"attempt": 1
}
]One error shape, everywhere
Chat, media, a pipeline run — every failing response carries the same envelope, so a client branches on structure once instead of per endpoint.
- type is one of six declared values: invalid_request_error, authentication_error, permission_error, quota_exceeded, rate_limit_error, server_error. Five of them ship today — a throttle you hit arrives as quota_exceeded, as above, and rate_limit_error is declared but not currently sent by any path, so don’t write a branch waiting for it.
- code is the stable string to branch on — rate_limit_exceeded here — because message is prose and gets reworded without notice.
- param names the request field the error is attributable to; null when it isn’t tied to one.
HTTP 403
{
"error": {
"message": "Rate limit exceeded: 23/20 requests per minute",
"type": "quota_exceeded",
"code": "rate_limit_exceeded",
"param": null
}
}Sync, async or streamed — one field picks
POST /v1/pipelines/runs takes a mode. Leave it unset and the call blocks until the run is done; the other two hand back control immediately.
- Unset (sync, the default): the response is the finished run, body and all — even a run that failed returns 201, with status: "failed" inside it. Only a request that never starts is a 4xx.
- mode: "async": the response is { id, status: "queued", createdAt } right away; poll GET /v1/pipelines/runs/{id} for the real status.
- mode: "stream": text/event-stream, not JSON — step.started, step.delta, step.completed and the rest, ending on pipeline.completed or pipeline.failed.
Frequently asked questions
Does a failed pipeline run return an error status code?
Only if it never starts — a bad body is a 400. Once a run starts, sync mode returns 201 even when the run itself ends status: "failed"; the run failing and the request failing are different things, and which one happened is inside the body, not the HTTP code.
Do I pay for a run that fails halfway through?
For the steps that already dispatched, yes — each one settles against the wallet as it runs, with no pre-flight hold and no cap taken from the /v1/pipelines/estimate quote. You don’t pay for it twice: resume_from_run_id continues a failed run from the step that failed, and the steps already recorded as succeeded are reused, not re-dispatched.
What happens if I send the same request twice?
Set an Idempotency-Key header. A second request carrying a key already used for that workspace returns the first run instead of executing again, in every mode; one still in flight under that key is a 409.
Which layer wins when two quota presets disagree?
The narrowest one that actually sets the field. Resolution runs platform default, then plan, then the key creator’s own preset, then the key’s own preset — a layer that leaves a field unset is skipped, not treated as zero. The last two only apply on a plan that carries custom quotas; without it a preset you attach to a member or a key resolves to nothing, and the plan’s own limits stand.
What does mode: 'stream' actually send?
Named SSE events — pipeline.started, step.started, step.delta, step.completed, step.failed, step.skipped, foreach.started, iteration.started, iteration.succeeded, iteration.failed, foreach.completed — then a terminal pipeline.completed or pipeline.failed, followed by data: [DONE]. It’s text/event-stream, not JSON.
Every endpoint, at this level of detail
The reference carries the rest: every route, every parameter, every status code. A new key comes with trial credits on it, no card, so the first request is a real one.