# Get run status

> Poll a run for its state and collect its outputs.

Canonical page: https://www.rundiffusion.com/docs/api/generate/status
Endpoint: GET /api/v2/generate/{request_id}
Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

---

> **Agents recommended**
>
> **Generate endpoints are built for agents, not ideal for static integrations.**
>
> We strongly advise against building a fixed, hand-coded integration on top of these endpoints.
>
> The tool catalog is a living, evolving, and dynamic list. Tools gain and lose fields, and tools themselves arrive and disappear, at any time. That is why every generate call carries a `tool_fields_hash`: it is a fingerprint of the exact field schema you built your inputs from. When a tool changes, your hash stops matching and the call answers `409 TOOL_SCHEMA_STALE` instead of running something you did not describe.
>
> An agent handles that in a way a static client cannot. It would be capable of refetching the tool with [Get a tool](/docs/api/tools/get), reads the new `tool_fields_hash` and the new fields, rebuilds the request, and sends it again. If a tool is gone, or no longer does the job it used to, an agent can pick a different one from [List tools](/docs/api/tools/list). That self-recovery is the assumption these endpoints are designed around.
>
> **An MCP server is coming very soon** that uses this API underneath. If you are building an agent, it will almost certainly be a better interface to build on than calling Generate directly.

Returns the current state of a run, and its outputs once it succeeds. Prefer a
[webhook](/docs/api/generate/webhooks) in production and use this to poll when
you cannot receive one.

`GET /api/v2/generate/{request_id}`

Authorization: OAuth device flow or Personal API Access Token or Company API Access Token

## Request

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer token. This endpoint accepts an OAuth device flow token, a Personal API Access Token, or a Company API Access Token. See [Authentication](/docs/api/authentication). |

**Path**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `request_id` | string | Yes | The request_id from the [create response](/docs/api/generate/create). Prefer following status_url, which already has it filled in. |

No query parameters and no body.

## Response

```json
{
  "request_id": "9f8e7d6c-…",
  "status": "succeeded",
  "created": "2026-05-04T10:00:00Z",
  "queued_at": "2026-05-04T10:00:01Z",
  "completed_at": "2026-05-04T10:00:09Z",
  "retry_after_seconds": null,
  "outputs": [
    {
      "type": "IMG",
      "url": "https://rundiffusion.com/...",
      "mime_type": "image/jpeg",
      "width": 1024,
      "height": 1024,
      "duration_seconds": null,
      "size_bytes": 244000,
      "expires_at": "2026-05-18T10:00:09Z",
      "expired": false
    }
  ],
  "metadata": { "trace_id": "client-trace-1" },
  "error": null,
  "tokens_charged": 85,
  "tokens_returned": 0
}
```

A terminal failure returns `200` too, with the reason in `error` and the hold
refunded when the failure was not the caller's fault:

```json
{
  "request_id": "9f8e7d6c-…",
  "status": "failed",
  "created": "2026-05-04T10:00:00Z",
  "queued_at": "2026-05-04T10:00:01Z",
  "completed_at": "2026-05-04T10:00:04Z",
  "retry_after_seconds": null,
  "outputs": [],
  "metadata": {},
  "error": {
    "code": "CONTENT_POLICY_VIOLATION",
    "message": "Provider or NSFW filter flagged the result.",
    "retryable": false
  },
  "tokens_charged": 85,
  "tokens_returned": 85
}
```

`status` is one of:

| Status | Meaning |
| --- | --- |
| `pending` | Accepted and queued. |
| `processing` | Running now. |
| `succeeded` | Finished. `outputs` holds the results. |
| `failed` | Terminal failure. `error` holds the envelope. |

Only `succeeded` and `failed` are terminal. While the run is in flight,
`retry_after_seconds` tells you how long to wait before polling again; it is
`null` once the run is terminal.

## Output fields

Output URLs are signed and expiring, and a run's objects live for about
**14 days** from creation. Every status fetch inside that window mints a fresh
signed URL, so re-fetch rather than storing one. Once `expires_at` passes, the
same endpoint returns `expired: true` and `url: null` and the bytes are gone, so
download anything you need to keep.

**Each entry in outputs**

| Name | Type | Description |
| --- | --- | --- |
| `type` | string | Output kind: IMG, VID, ASSET_3D, LAYERS. |
| `url` | string \| null | Signed download URL. Null when the output has no stored object or has expired. |
| `mime_type` | string \| null | MIME type of the output. |
| `width` | integer \| null | Pixel width, for visual output. |
| `height` | integer \| null | Pixel height, for visual output. |
| `duration_seconds` | number \| null | Duration, for video or audio output. |
| `size_bytes` | integer \| null | Size of the output file in bytes. |
| `expires_at` | string (ISO 8601) \| null | When the signed URL stops working. |
| `expired` | boolean | Whether the URL has already expired. |

`tokens_charged` is what the run actually cost, and `tokens_returned` is any
refund for results that failed to produce.

## Errors

| Code | What to do |
| --- | --- |
| `REQUEST_NOT_FOUND` | Returns `404`. No such `request_id`, or not one this credential can see. A run belonging to somebody else reads the same as one that never existed. |
| `RATE_LIMITED` | Returns `429`. See [Rate limits](/docs/api/rate-limits#generation). See [Rate limits](/docs/api/rate-limits#generation). |

The full list is in [Errors](/docs/api/errors).
