openapi: 3.0.3
info:
  title: SOAT Files API
  version: 1.0.0
  description: API for managing files (Files resource)
  contact:
    name: SOAT Team
    url: https://github.com/ttoss/soat
servers:
  - url: '{baseUrl}'
    description: Base URL of your SOAT deployment (e.g. https://your-soat.com or http://localhost:5047)
    variables:
      baseUrl:
        description: The base URL of your SOAT deployment
        default: http://localhost:5047
tags:
  - name: Files
    description: Manage files
security:
  - bearerAuth: []
paths:
  /api/v1/files:
    get:
      tags:
        - Files
      summary: List all files
      description: Returns a list of all stored files
      operationId: listFiles
      parameters:
        - name: project_id
          in: query
          required: false
          description: Filter files by project ID
          schema:
            type: string
            example: 'proj_V1StGXR8Z5jdHi6B'
        - name: path_prefix
          in: query
          required: false
          description: >-
            Only files under this directory. The prefix is a path boundary, not
            a substring: `/reports` returns `/reports/q1.txt` and never
            `/reports-archive/q1.txt`, and `/` selects the whole project. A
            leading slash is optional and a trailing one is ignored, so
            `reports`, `/reports` and `/reports/` are the same filter. `%` and
            `_` are literal characters, not wildcards. Naming a directory under
            `/.system/` is what includes platform-written files, which a list
            without this parameter leaves out.
          schema:
            type: string
            example: '/reports/'
        - $ref: './tags.yaml#/components/parameters/TagsQuery'
        - $ref: './pagination.yaml#/components/parameters/Limit'
        - $ref: './pagination.yaml#/components/parameters/Offset'
      responses:
        '200':
          description: List of files returned successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/FileRecord'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
    post:
      tags:
        - Files
      summary: Create a file
      description: Creates a new file record in the system
      operationId: createFile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Public ID of the project. Optional when authenticating with a project-scoped API key, which defaults to the key's project; required otherwise.
                  example: 'proj_V1StGXR8Z5jdHi6B'
                prefix:
                  type: string
                  description: Directory within the project (e.g. /images). Optional; defaults to / (root). Combined with filename to form the file's key (path).
                  example: '/images'
                filename:
                  type: string
                  description: Original / download name and the key's leaf segment (e.g. logo.png).
                  example: 'logo.png'
                content_type:
                  type: string
                  description: MIME type of the file
                  example: 'application/pdf'
                size:
                  type: integer
                  nullable: true
                  description: File size in bytes
                  example: 1024
                metadata:
                  $ref: './metadata.yaml#/components/schemas/MetadataBag'
      responses:
        '201':
          description: File created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '409':
          description: >-
            The project's `storage_bytes` quota is exceeded
            (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the
            quota — no window reset clears a stored total, so no `Retry-After`
            is sent.
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/upload:
    post:
      tags:
        - Files
      summary: Upload a file
      description: Uploads a file to the server and stores it in the configured storage directory
      operationId: uploadFile
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: File content
                project_id:
                  x-soat-ref: projects
                  type: string
                  description: Project ID to associate the file with. Optional when authenticating with a project-scoped API key, which defaults to the key's project; required otherwise.
                  example: 'proj_V1StGXR8Z5jdHi6B'
                prefix:
                  type: string
                  description: Directory within the project (e.g. /images). Optional; defaults to / (root).
                  example: '/images'
                filename:
                  type: string
                  description: Original / download name. Optional; defaults to the uploaded file's name.
                  example: 'logo.png'
                metadata:
                  $ref: './metadata.yaml#/components/schemas/MetadataBagText'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing file or invalid project
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '409':
          description: >-
            The project's `storage_bytes` quota is exceeded
            (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the
            quota — no window reset clears a stored total, so no `Retry-After`
            is sent.
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '413':
          description: >-
            The upload is over this deployment's byte ceiling
            (`UPLOAD_TOO_LARGE`; `FILE_UPLOAD_MAX_BYTES`, 25 MB by default).
            The request is refused while the body is still streaming, so
            nothing was stored.
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/upload/base64:
    post:
      tags:
        - Files
      summary: Upload a file using base64 encoding
      description: Uploads a file to the server using base64-encoded content
      operationId: uploadFileBase64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadFileBase64Request'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing content or invalid project
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '409':
          description: >-
            The project's `storage_bytes` quota is exceeded
            (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or raise the
            quota — no window reset clears a stored total, so no `Retry-After`
            is sent.
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/presigned-url:
    post:
      tags:
        - Files
      summary: Create a presigned upload URL
      description: >-
        Creates a short-lived, single-use presigned upload URL — the
        local-storage equivalent of an S3 presigned URL. The client then uploads
        the file content directly to the returned `upload_url` via
        `POST /api/v1/files/upload/{token}`, bypassing MCP payload size limits.
        When the server is configured with `SOAT_BASE_URL`, `upload_url` is a
        fully-qualified absolute URL so MCP agents and other clients can POST to
        it without knowing the server base URL in advance.
      operationId: createPresignedUrl
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PresignedUrlRequest'
      responses:
        '201':
          description: Presigned URL created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PresignedUrlResponse'
        '400':
          description: Missing or invalid project
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
  /api/v1/files/upload/{token}:
    post:
      tags:
        - Files
      summary: Upload a file using an upload token
      description: >-
        Uploads file content authorized by a single-use token from
        `POST /api/v1/files/presigned-url`. No bearer credential is required —
        the token is the credential. Accepts either multipart/form-data (field
        `file`) or JSON with a base64-encoded `content` field.
      operationId: uploadFileWithToken
      security: []
      parameters:
        - name: token
          in: path
          required: true
          description: The single-use upload token (e.g. upt_...)
          schema:
            type: string
            example: 'upt_V1StGXR8Z5jdHi6B'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: File content
                metadata:
                  $ref: './metadata.yaml#/components/schemas/MetadataBagText'
          application/json:
            schema:
              $ref: '#/components/schemas/UploadFileWithTokenRequest'
      responses:
        '201':
          description: File uploaded successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '400':
          description: Missing content
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '404':
          description: Upload token not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '409':
          description: >-
            Upload token already used, or the project's `storage_bytes` quota
            is exceeded (`QUOTA_STORAGE_EXCEEDED`)
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '410':
          description: Upload token expired
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '413':
          description: >-
            The upload is over this deployment's byte ceiling
            (`UPLOAD_TOO_LARGE`; `FILE_UPLOAD_MAX_BYTES`, 25 MB by default).
            The request is refused while the body is still streaming, so
            nothing was stored.
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}:
    get:
      tags:
        - Files
      summary: Get a file by ID
      description: Returns the data and metadata of a specific file
      operationId: getFile
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
            example: 'abc123'
      responses:
        '200':
          description: File found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
    delete:
      tags:
        - Files
      summary: Delete a file
      description: Removes a file from the system by ID
      operationId: deleteFile
      parameters:
        - name: file_id
          in: path
          required: true
          description: ID of the file to delete
          schema:
            type: string
            example: 'abc123'
      responses:
        '204':
          description: File deleted successfully
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/download:
    get:
      tags:
        - Files
      summary: Download a file
      description: Streams the file content to the client
      operationId: downloadFile
      # Not a tool: a tool result is a JSON value, and this route's is a stream
      # of raw bytes. Advertising it would offer callers — MCP clients and
      # agents alike — an action that can only ever fail, and spend a tool slot
      # and its schema tokens doing so. `downloadFileBase64` is the callable
      # form and returns the same content.
      x-soat-mcp-exclude: true
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
        - name: token
          in: query
          required: false
          description: >-
            Signed single-file download token, an alternative to a bearer
            credential (issued for ingestion-rule converters).
          schema:
            type: string
            example: 'file_abc123'
      responses:
        '200':
          description: File content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/metadata:
    patch:
      tags:
        - Files
      summary: Update file metadata
      description: Updates the metadata field of a file
      operationId: updateFileMetadata
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
            example: 'file_abc123'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                metadata:
                  description: Replaces the stored bag; `null` clears it.
                  allOf:
                    - $ref: './metadata.yaml#/components/schemas/NullableMetadataBag'
                prefix:
                  type: string
                  description: New directory — moves the file. The resulting path (prefix + filename) must be unique within the project.
                  example: '/reports'
                filename:
                  type: string
                  description: New filename — renames the key's leaf and the download name.
                  example: 'renamed-file.txt'
      responses:
        '200':
          description: Metadata updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileRecord'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
        '409':
          description: A file already exists at the target path in this project
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/download/base64:
    get:
      tags:
        - Files
      summary: Download file as base64
      description: Returns the file content encoded as base64
      operationId: downloadFileBase64
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
        - name: token
          in: query
          required: false
          description: >-
            Signed single-file download token, an alternative to a bearer
            credential (issued for ingestion-rule converters).
          schema:
            type: string
      responses:
        '200':
          description: File content as base64
          content:
            application/json:
              schema:
                type: object
                properties:
                  content:
                    type: string
                    description: Base64-encoded file content
                  filename:
                    type: string
                    description: Original filename
                  content_type:
                    type: string
                    description: MIME type of the file
                  size:
                    type: integer
                    nullable: true
                    description: File size in bytes
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
  /api/v1/files/{file_id}/tags:
    get:
      tags:
        - Files
      summary: Get file tags
      description: Returns all tags attached to the file
      operationId: getFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      responses:
        '200':
          description: File tags
          content:
            application/json:
              schema:
                $ref: './tags.yaml#/components/schemas/TagBag'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
    put:
      tags:
        - Files
      summary: Replace file tags
      description: Replaces all tags on the file with the provided tags
      operationId: replaceFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: './tags.yaml#/components/schemas/TagBag'
      responses:
        '200':
          description: Tags replaced
          content:
            application/json:
              schema:
                $ref: './tags.yaml#/components/schemas/TagBag'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
    patch:
      tags:
        - Files
      summary: Merge file tags
      description: Merges provided tags into the file's existing tags (existing tags are preserved unless overridden)
      operationId: mergeFileTags
      parameters:
        - name: file_id
          in: path
          required: true
          description: File ID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: './tags.yaml#/components/schemas/TagBag'
      responses:
        '200':
          description: Tags merged
          content:
            application/json:
              schema:
                $ref: './tags.yaml#/components/schemas/TagBag'
        '401':
          $ref: './errors.yaml#/components/responses/Unauthorized'
        '403':
          $ref: './errors.yaml#/components/responses/Forbidden'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: './errors.yaml#/components/schemas/ErrorResponse'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: JWT token or sk_ api key
  schemas:
    UploadFileBase64Request:
      type: object
      required:
        - content
      additionalProperties: false
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
          example: 'proj_V1StGXR8Z5jdHi6B'
        content:
          type: string
          description: Base64-encoded file content
          example: 'SGVsbG8gV29ybGQ='
        prefix:
          type: string
          description: Directory within the project (e.g. /documents). Optional; defaults to / (root).
          example: '/documents'
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: 'document.txt'
        content_type:
          type: string
          description: MIME type of the file
          example: 'text/plain'
        metadata:
          $ref: './metadata.yaml#/components/schemas/MetadataBag'
    PresignedUrlRequest:
      type: object
      required:
        - project_id
      additionalProperties: false
      properties:
        project_id:
          x-soat-ref: projects
          type: string
          description: Public ID of the project
          example: 'proj_V1StGXR8Z5jdHi6B'
        prefix:
          type: string
          description: Directory within the project (e.g. /documents). Optional; defaults to / (root).
          example: '/documents'
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: 'report.pdf'
        content_type:
          type: string
          description: MIME type of the file to be uploaded
          example: 'application/pdf'
    PresignedUrlResponse:
      type: object
      properties:
        upload_token:
          type: string
          description: Single-use token value
          example: 'upt_V1StGXR8Z5jdHi6B'
        upload_url:
          type: string
          description: >-
            Fully-qualified URL to POST the file content to (absolute when
            SOAT_BASE_URL is configured, relative otherwise).
          example: 'https://api.example.com/api/v1/files/upload/upt_V1StGXR8Z5jdHi6B'
        expires_at:
          type: string
          format: date-time
          description: Token expiration timestamp
    UploadFileWithTokenRequest:
      type: object
      additionalProperties: false
      properties:
        content:
          type: string
          description: Base64-encoded file content (alternative to multipart `file`)
          example: 'SGVsbG8gV29ybGQ='
        filename:
          type: string
          description: Original / download name (overrides the token's filename)
          example: 'document.txt'
        content_type:
          type: string
          description: MIME type of the file (overrides the token's content type)
          example: 'text/plain'
        metadata:
          $ref: './metadata.yaml#/components/schemas/MetadataBag'
    FileRecord:
      type: object
      description: Stored file metadata
      properties:
        id:
          type: string
          description: Unique file identifier
          example: 'abc123'
        prefix:
          type: string
          readOnly: true
          description: Directory of the file (the `path` without its last segment). Read-only — set it via `prefix` on write.
          example: '/images'
        filename:
          type: string
          description: Original / download name and the key's leaf segment.
          example: 'logo.png'
        path:
          type: string
          nullable: true
          readOnly: true
          description: Full key of the file within the project — `prefix` + `/` + `filename` (e.g. /images/logo.png). Read-only; unique per project; the file's identity and policy-SRN target.
          example: '/images/logo.png'
        content_type:
          type: string
          nullable: true
          description: MIME type of the file
          example: 'application/pdf'
        size:
          type: integer
          nullable: true
          description: File size in bytes
          example: 1024
        metadata:
          $ref: './metadata.yaml#/components/schemas/MetadataBag'
        tags:
          $ref: './tags.yaml#/components/schemas/TagBag'
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        updated_at:
          type: string
          format: date-time
          description: Last update timestamp
