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

# Stream one artifact's bytes, verified against its journaled SHA-256

> `{name}` is an entry's `name` from GET /v1/jobs/{id}/artifacts, never a filesystem path. Every name contains `/` (`file/out/report.pdf`, `agent_image/1.png`): send it with its `/` literal or percent-encoded (`%2F`), as one path parameter. The rest of the path is percent-decoded once and matched exactly against the manifest's names; an escape that is not two hexadecimal digits, bytes that are not UTF-8 or NUL match no entry. The server opens the recorded place below the project root it holds, refusing a symbolic link at any component, and reads every byte against the recorded size and SHA-256 before the response starts (409 `artifact_changed` on a mismatch), keeping the SHA-256 of each 256 KiB chunk. While the bytes stream, each chunk is read again and sent only if it hashes to the digest kept for it: every byte that leaves is a byte of the recorded file, and a file changed during the stream ends it before its `Content-Length`. A stream also ends short when the server stops or cannot read the file: retry a short body, and a file that really changed then answers 409 `artifact_changed`. The bound is `max_artifact_bytes`; a larger artifact stays listed and is refused 403. At most 4 journal verifications and pre-download hash passes run at once (`artifact_capacity` otherwise); a stream holds no slot once its bytes start, is bounded by the connection limit, and is not bounded by the request deadline.



## OpenAPI

````yaml /api-reference/openapi.json get /v1/jobs/{id}/artifacts/{name}
openapi: 3.1.0
info:
  description: >-
    Authenticated loopback remote execution, declarative schedules, verified
    downloads of the artifacts a settled job's sealed journal records, and
    stateless source-only authoring (POST /v1/compile never runs, admits or
    writes). Schedule list/delete/trigger/backfill, /v1/arm, and POST /v1/run
    are absent.
  title: nika serve
  version: 0.124.1
servers:
  - url: http://127.0.0.1
security:
  - bearerAuth: []
paths:
  /v1/jobs/{id}/artifacts/{name}:
    get:
      summary: Stream one artifact's bytes, verified against its journaled SHA-256
      description: >-
        `{name}` is an entry's `name` from GET /v1/jobs/{id}/artifacts, never a
        filesystem path. Every name contains `/` (`file/out/report.pdf`,
        `agent_image/1.png`): send it with its `/` literal or percent-encoded
        (`%2F`), as one path parameter. The rest of the path is percent-decoded
        once and matched exactly against the manifest's names; an escape that is
        not two hexadecimal digits, bytes that are not UTF-8 or NUL match no
        entry. The server opens the recorded place below the project root it
        holds, refusing a symbolic link at any component, and reads every byte
        against the recorded size and SHA-256 before the response starts (409
        `artifact_changed` on a mismatch), keeping the SHA-256 of each 256 KiB
        chunk. While the bytes stream, each chunk is read again and sent only if
        it hashes to the digest kept for it: every byte that leaves is a byte of
        the recorded file, and a file changed during the stream ends it before
        its `Content-Length`. A stream also ends short when the server stops or
        cannot read the file: retry a short body, and a file that really changed
        then answers 409 `artifact_changed`. The bound is `max_artifact_bytes`;
        a larger artifact stays listed and is refused 403. At most 4 journal
        verifications and pre-download hash passes run at once
        (`artifact_capacity` otherwise); a stream holds no slot once its bytes
        start, is bounded by the connection limit, and is not bounded by the
        request deadline.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
        - description: >-
            An entry's manifest `name`; its `/` may be sent literally or as
            `%2F` (decoded once, matched exactly)
          in: path
          name: name
          required: true
          schema:
            minLength: 1
            type: string
      responses:
        '200':
          content:
            '*/*':
              schema:
                contentMediaType: application/octet-stream
                description: The artifact's bytes; Content-Type is the manifest's `mime`
                type: string
          description: The recorded bytes
          headers:
            Cache-Control:
              schema:
                const: no-store
                type: string
            Content-Disposition:
              schema:
                const: attachment
                type: string
            Content-Length:
              description: The recorded size
              schema:
                minimum: 0
                type: integer
            Content-Security-Policy:
              schema:
                const: sandbox
                type: string
            Nika-Artifact-Sha256:
              description: The recorded SHA-256 (lowercase hex), the manifest's `sha256`
              schema:
                pattern: ^[0-9a-f]{64}$
                type: string
            X-Content-Type-Options:
              schema:
                const: nosniff
                type: string
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Error envelope
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: '`artifact_too_large`: over this server''s download bound'
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: >-
            `job_not_found`: unknown or foreign job id · `artifact_not_found`:
            no entry by that name
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: >-
            `job_not_settled` · `artifact_journal_unavailable` ·
            `artifact_journal_unverified` (as the manifest) ·
            `artifact_changed`: the bytes at the recorded place no longer match
            the record
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: >-
            `internal_error`: the server could not read the journal or the
            artifact
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: '`store_busy` · `artifact_capacity`'
components:
  schemas:
    Error:
      additionalProperties: false
      properties:
        error:
          additionalProperties: false
          properties:
            code:
              type: string
            message:
              type: string
          required:
            - code
            - message
          type: object
      required:
        - error
      type: object
  securitySchemes:
    bearerAuth:
      description: 'Exactly one Authorization: Bearer value from the token file'
      scheme: bearer
      type: http

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.