States
Every request progresses through one of these statuses:COMPLETED, FAILED, and CANCELLED are terminal — the request will never transition away from them.
Two things about terminal states that are easy to get wrong:A failed generation is recorded as
COMPLETED, not FAILED. When the provider errors, the request itself completed — it just produced no output — so it lands on COMPLETED with a populated error and a billingStatus of failed. FAILED is reserved for requests the platform force-fails after they get stuck. So status alone does not tell you whether the work succeeded; check billingStatus (charged or partial means real output).CANCELLED is never written today. Nothing sets it, and cancel_url does not cancel (see Cancellation). Handle it defensively if you like, but do not build a flow that waits for it.Submitting a request
request_id, status: "IN_QUEUE", and three URLs:
Tagging requests with metadata
The submit body may include a reserved top-levelmetadata object alongside the model’s input fields — a flat string map of your own tags (job ids, environments, batch labels):
GET /requests/{requestId}, the response_url result payload, and list items. (The lightweight status_url envelope does not include it.) It is never sent to the model and never merged into the stored input.
Limits (violations return 400):
Filter your request history by metadata with a single URL-encoded JSON query param — pairs match exactly and are AND-ed, combinable with
status, modelEndpoint, and pagination:
metadata is a reserved word at the top level of the submit body: a model whose own input schema defines a metadata field cannot receive it through the raw body.Three ways to watch a request
1. Server-Sent Events (recommended for UIs)
1. Server-Sent Events (recommended for UIs)
Open one connection to
GET /requests/stream and receive push updates for every in-flight request the user owns. On connect, the server emits a snapshot event with current state; thereafter each status transition emits an update event. A : hb SSE comment is sent every 25 seconds to keep the connection alive — your client can ignore it.This eliminates the polling loop entirely. Best for dashboards, multi-request UIs, and any client that opens multiple requests in parallel.2. Webhooks (recommended for server-to-server)
2. Webhooks (recommended for server-to-server)
Pass a Deliveries are signed with Standard Webhooks HMAC-SHA256, retried for about two hours, and inspectable after the fact. See Webhooks for the payload, verification snippets and the retry schedule.
webhook URL in the submit body and we POST the result to it when the request settles — no polling loop and no long-lived connection, so nothing is lost if your process restarts mid-request.3. Polling
3. Polling
GET /{ownerName}/{modelName}/requests/{requestId}/status returns the same response shape as the create call. Poll at 1–2 second intervals. The SDK helpers (subscribe in JS, submit_async + iter_events in Python) wrap this loop for you.Cancellation
GET /{ownerName}/{modelName}/requests/{requestId}/cancel returns the request’s current status payload.
Platform safety net: automatic finalization
You do not need to implement retry logic for requests you’ve abandoned. A background sweep runs every 60 seconds and re-checks every request that is:- Still
IN_PROGRESSpast the provider’s expected duration, or - Marked
COMPLETEDby the provider but missing output media (media upload still in flight).
FAILED and any associated billing is reverted.
Billing tie-in
Billing settles on the same lifecycle:- A request transitioning to
COMPLETEDcharges the user’s balance. - A request transitioning to
FAILEDorCANCELLEDis not charged. - A request that completed at the provider but failed schema validation is recorded with
billingStatus: "failed"— you are not billed, but the call returns422so you can surface the upstream error.
422 payload shape.
