> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.nunu.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Artifact File

> Stream a single artifact file produced by a run. The response body is the raw file bytes (not JSON). Range requests, conditional requests via ETag, and presigned redirects for large media files (e.g. .mp4 recordings) are supported.

<Info>
  **Permission Required**: any API key scoped to the project that owns the run
</Info>

<Note>
  Artifact endpoints are served by the **bouncer** service at
  `https://bouncer.nunu.ai`, not the main `nunu.ai/api/v1` host. Use the same
  `X-Api-Key` header for authentication.
</Note>

Stream a single artifact file directly from object storage. The response is the
raw file bytes with appropriate `Content-Type` and `Content-Length` headers,
not a JSON envelope.

The endpoint supports HTTP range requests, conditional requests via `ETag`, and
serves a presigned redirect for large media files (such as `.mp4` recordings)
to allow efficient streaming.

## Path Parameters

<ParamField path="runId" type="string" required>
  Composite run ID, in the format `{multiplayerRunId}-{playerNumber}` (for
  example `lkkg6t5612m-1`).
</ParamField>

<ParamField path="filename" type="string" required>
  Artifact filename, exactly as returned by
  [List Run Artifacts](/api-reference/artifacts/list-artifacts). May include
  subdirectories (for example `deliverables/report.json`).
</ParamField>

## Headers

<ParamField header="Range" type="string">
  Standard HTTP range header. The endpoint returns `206 Partial Content` for
  satisfiable ranges and `416 Range Not Satisfiable` otherwise.
</ParamField>

<ParamField header="If-None-Match" type="string">
  Send the ETag from a previous response to receive `304 Not Modified` when
  the file has not changed.
</ParamField>

## Response

The response body is the raw file content. Common status codes:

| Status | Meaning                                                       |
| ------ | ------------------------------------------------------------- |
| `200`  | Full file body                                                |
| `206`  | Partial content (response to a `Range` request)               |
| `302`  | Redirect to a presigned URL (typically for large media files) |
| `304`  | Not modified (when `If-None-Match` matches)                   |
| `404`  | Artifact does not exist or is hidden from the requesting key  |
| `416`  | Requested range cannot be satisfied                           |

<Tip>
  When following the URL returned by
  [List Run Artifacts](/api-reference/artifacts/list-artifacts), you typically
  do not need to construct this path manually—just GET that URL with your API
  key.
</Tip>

<RequestExample>
  ```bash cURL theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X GET \
    "https://bouncer.nunu.ai/runs/lkkg6t5612m-1/artifacts/device_log.log" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -o device_log.log
  ```

  ```bash cURL (range request) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X GET \
    "https://bouncer.nunu.ai/runs/lkkg6t5612m-1/artifacts/recording.mp4" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Range: bytes=0-1048575" \
    -o recording-part.mp4
  ```

  ```typescript JavaScript theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  const runId = "lkkg6t5612m-1";
  const filename = "device_log.log";

  const response = await fetch(
    `https://bouncer.nunu.ai/runs/${runId}/artifacts/${filename}`,
    {
      headers: {
        "X-Api-Key": process.env.NUNU_API_TOKEN,
      },
    }
  );

  if (!response.ok) {
    throw new Error(`Failed to download: ${response.status}`);
  }

  const buffer = await response.arrayBuffer();
  ```

  ```python Python theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  import requests
  import os

  run_id = "lkkg6t5612m-1"
  filename = "device_log.log"

  response = requests.get(
      f"https://bouncer.nunu.ai/runs/{run_id}/artifacts/{filename}",
      headers={"X-Api-Key": os.environ["NUNU_API_TOKEN"]},
      stream=True,
  )
  response.raise_for_status()

  with open(filename, "wb") as f:
      for chunk in response.iter_content(chunk_size=1 << 16):
          f.write(chunk)
  ```
</RequestExample>


## OpenAPI

````yaml GET /runs/{runId}/artifacts/{filename}
openapi: 3.1.0
info:
  title: nunu.ai API
  description: >-
    The nunu.ai Public API allows you to programmatically interact with your
    projects, manage test runs, and upload builds.
  version: 1.0.0
servers:
  - url: https://nunu.ai/api/v1
security:
  - apiKeyAuth: []
paths:
  /runs/{runId}/artifacts/{filename}:
    servers:
      - url: https://bouncer.nunu.ai
        description: Bouncer service (artifact and knowledge delivery)
    get:
      tags:
        - Artifacts
      summary: Get Artifact File
      description: >-
        Stream a single artifact file produced by a run. The response body is
        the raw file bytes (not JSON). Range requests, conditional requests via
        ETag, and presigned redirects for large media files (e.g. .mp4
        recordings) are supported.
      operationId: getRunArtifact
      parameters:
        - name: runId
          in: path
          required: true
          description: Composite run ID, in the format {multiplayerRunId}-{playerNumber}.
          schema:
            type: string
        - name: filename
          in: path
          required: true
          description: >-
            Artifact filename, exactly as returned by List Run Artifacts. May
            include subdirectories (e.g. deliverables/report.json).
          schema:
            type: string
        - name: Range
          in: header
          required: false
          description: >-
            Standard HTTP range header. Returns 206 for satisfiable ranges and
            416 otherwise.
          schema:
            type: string
        - name: If-None-Match
          in: header
          required: false
          description: >-
            Send the ETag from a previous response to receive 304 Not Modified
            when the file has not changed.
          schema:
            type: string
      responses:
        '200':
          description: >-
            Full file body. Content-Type is set based on the artifact's stored
            type.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '206':
          description: Partial content (response to a Range request)
        '302':
          description: Redirect to a presigned URL (typically for large media files)
        '304':
          description: Not modified (when If-None-Match matches the current ETag)
        '401':
          description: Unauthorized - missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - API key does not match the run's project
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Artifact does not exist or is hidden from the requesting key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '416':
          description: Requested range cannot be satisfied
components:
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: API key for authentication

````