openapi: 3.1.0
info:
  title: CometCMS Public API
  version: "1.0.0"
  description: Stable HTTP API for headless content, media, and integration scripts. Workspace-scoped routes are available under /api/v1/workspaces/{workspace} with the same content, schema, and media paths.
servers:
  - url: https://yourdomain.com/api/v1
security: []
tags:
  - name: Health
  - name: Content Types
  - name: Content
  - name: Media
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API token created in the admin UI with permission grants.
  parameters:
    Collection:
      name: collection
      in: path
      required: true
      schema:
        type: string
        pattern: "^[A-Za-z0-9_-]+$"
    Identifier:
      name: identifier
      in: path
      required: true
      description: Entry slug or stable opaque id.
      schema:
        type: string
        pattern: "^[A-Za-z0-9_-]+$"
    Filename:
      name: filename
      in: path
      required: true
      schema:
        type: string
    Limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
      description: Maximum number of items. Omit to return all matching items.
    Offset:
      name: offset
      in: query
      schema:
        type: integer
        minimum: 0
      description: Number of matching items to skip.
    Locale:
      name: locale
      in: query
      schema:
        type: string
        pattern: "^[A-Za-z0-9_-]+$"
      description: Locale code for localized content types. Resolves translated values before search, filters, sorting, and relation expansion.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string
            fields:
              type: object
              additionalProperties:
                type: string
    ListMeta:
      type: object
      required: [total, limit, offset]
      properties:
        total:
          type: integer
          minimum: 0
        limit:
          type: integer
          minimum: 0
        offset:
          type: integer
          minimum: 0
        sort:
          type: string
        order:
          type: string
          enum: [asc, desc]
    ContentType:
      type: object
      required:
        [
          name,
          label,
          icon,
          singleton,
          slug_field,
          slug_source,
          locales,
          default_locale,
          fields,
        ]
      properties:
        name:
          type: string
        label:
          type: string
        icon:
          type: string
        singleton:
          type: boolean
          description: When true, the content type represents one fixed entry whose slug matches the content type name.
        slug_field:
          type: string
        slug_source:
          type: string
        locales:
          type: array
          items:
            type: string
        default_locale:
          type: string
        fields:
          type: object
          description: Field definitions. Supported field types may include a `default` value, which is applied when creating entries with that field omitted.
          additionalProperties:
            type: object
            additionalProperties: true
    ContentTypeCreateInput:
      type: object
      required: [name]
      properties:
        name:
          type: string
        label:
          type: string
        icon:
          type: string
        singleton:
          type: boolean
        fields:
          type: object
          additionalProperties:
            type: object
            additionalProperties: true
        locales:
          type: array
          items:
            type: string
        default_locale:
          type: string
    ContentTypeUpdateInput:
      type: object
      properties:
        name:
          type: string
          description: Ignored; the path collection is used.
        label:
          type: string
        icon:
          type: string
        singleton:
          type: boolean
        fields:
          type: object
          additionalProperties:
            type: object
            additionalProperties: true
        locales:
          type: array
          items:
            type: string
        default_locale:
          type: string
    Entry:
      type: object
      required: [id, slug, type, status, title, created_at, updated_at, data]
      properties:
        id:
          type: string
          description: Stable opaque entry identifier.
        slug:
          type: string
          description: Human-readable URL key. May change.
        type:
          type: string
        status:
          type: string
          enum: [draft, published, scheduled, protected, archived]
        title:
          type: string
        published_at:
          type: [string, "null"]
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        author_id:
          type: string
        updated_by:
          type: string
        data:
          type: object
          additionalProperties: true
    MediaItem:
      type: object
      required:
        [filename, name, url, size, mime, category, alt, title, visibility]
      properties:
        filename:
          type: string
        name:
          type: string
        url:
          type: string
          format: uri
        size:
          type: integer
          minimum: 0
        mime:
          type: string
        category:
          type: string
        alt:
          type: string
        title:
          type: string
        visibility:
          type: string
          enum: [public, private]
        width:
          type: [integer, "null"]
          minimum: 1
        height:
          type: [integer, "null"]
          minimum: 1
        uploaded_by:
          type: [string, "null"]
        uploaded_at:
          type: [string, "null"]
          format: date-time
    Ok:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
paths:
  /health:
    get:
      tags: [Health]
      summary: Health check
      responses:
        "200":
          description: CMS health state.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [ok, name, version, time, extensions]
                    properties:
                      ok:
                        type: boolean
                      name:
                        type: string
                      version:
                        type: string
                      time:
                        type: string
                        format: date-time
                      extensions:
                        type: object
                        required: [gd, zip]
                        properties:
                          gd:
                            type: boolean
                            description: True when GD thumbnail generation functions are available.
                          zip:
                            type: boolean
                            description: True when zip archive support is available for backup/restore operations.
  /content-types:
    get:
      tags: [Content Types]
      summary: List content type schemas
      responses:
        "200":
          description: Content type schemas.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/ContentType"
    post:
      tags: [Content Types]
      summary: Create a content type schema
      description: Requires `schema.create` on `schema:{name}`.
      x-required-permission:
        effect: allow
        actions: [schema.create]
        resources: ["schema:{name}"]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContentTypeCreateInput"
      responses:
        "201":
          description: Content type created.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/ContentType"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `schema.create` permission for the content type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /content-types/{collection}:
    get:
      tags: [Content Types]
      summary: Get one content type schema
      parameters:
        - $ref: "#/components/parameters/Collection"
      responses:
        "200":
          description: Content type schema.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/ContentType"
        "404":
          description: Content type not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      tags: [Content Types]
      summary: Update a content type schema
      description: Requires `schema.update` on `schema:{collection}`.
      x-required-permission:
        effect: allow
        actions: [schema.update]
        resources: ["schema:{collection}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ContentTypeUpdateInput"
      responses:
        "200":
          description: Content type updated.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/ContentType"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `schema.update` permission for the content type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Content type not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [Content Types]
      summary: Delete a content type schema
      description: Permanently deletes the content type and its entries. Requires `schema.delete` on `schema:{collection}`.
      x-required-permission:
        effect: allow
        actions: [schema.delete]
        resources: ["schema:{collection}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
      responses:
        "200":
          description: Content type deleted.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Ok"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `schema.delete` permission for the content type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Content type not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /content/{collection}:
    get:
      tags: [Content]
      summary: List content entries
      description: Without a token, only public entries are returned. With `content.read` on `content:{collection}:*`, drafts and protected entries are included.
      x-required-permission:
        effect: allow
        actions: [content.read]
        resources: ["content:{collection}:*"]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/Collection"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
        - name: sort
          in: query
          schema:
            type: string
          description: Field to sort by. Prefix with `-` for descending.
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
        - name: q
          in: query
          schema:
            type: string
        - name: include
          in: query
          schema:
            type: string
          description: Comma-separated relation fields to expand one level.
        - $ref: "#/components/parameters/Locale"
        - name: filter[field]
          in: query
          style: form
          explode: true
          schema:
            type: object
            additionalProperties: true
          description: Field filters, including `in`, `ne`, `gt`, `gte`, `lt`, `lte`, and `contains` operators.
      responses:
        "200":
          description: Entry list.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Entry"
                  meta:
                    $ref: "#/components/schemas/ListMeta"
        "401":
          description: Invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `content.read` permission for the collection.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Content collection not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [Content]
      summary: Create a content entry
      description: Requires `content.create` on `content:{collection}:*`. If the payload sets `status` to `published`, also requires `content.publish` on the entry. For localized content types, include `locale` in the JSON body to create that locale variant. The default locale is used when omitted. For singleton content types, only one active entry may exist and its slug is forced to the content type name.
      x-required-permission:
        - effect: allow
          actions: [content.create]
          resources: ["content:{collection}:*"]
        - effect: allow
          actions: [content.publish]
          resources: ["content:{collection}:*"]
          when: status is published
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "201":
          description: Entry created.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Entry"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing required content permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /content/{collection}/{identifier}:
    get:
      tags: [Content]
      summary: Get one content entry
      description: Public reads return only public entries. Reading drafts, protected entries, or entries hidden by status requires `content.read` on `content:{collection}:{identifier}`.
      x-required-permission:
        effect: allow
        actions: [content.read]
        resources: ["content:{collection}:{identifier}"]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - $ref: "#/components/parameters/Collection"
        - $ref: "#/components/parameters/Identifier"
        - name: include
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/Locale"
      responses:
        "200":
          description: Entry.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Entry"
        "401":
          description: Invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `content.read` permission for the entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Entry or collection not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      tags: [Content]
      summary: Update a content entry
      description: Requires `content.update` on `content:{collection}:{identifier}`. If the payload sets `status` to `published`, also requires `content.publish` on the entry. For localized content types, include `locale` in the JSON body to update that locale variant. Slug, status, author, and publish date remain shared.
      x-required-permission:
        - effect: allow
          actions: [content.update]
          resources: ["content:{collection}:{identifier}"]
        - effect: allow
          actions: [content.publish]
          resources: ["content:{collection}:{identifier}"]
          when: status is published
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
        - $ref: "#/components/parameters/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Entry updated.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Entry"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing required content permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Entry or collection not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Content]
      summary: Update a content entry
      description: Same behavior and permissions as `PUT /content/{collection}/{identifier}`.
      x-required-permission:
        - effect: allow
          actions: [content.update]
          resources: ["content:{collection}:{identifier}"]
        - effect: allow
          actions: [content.publish]
          resources: ["content:{collection}:{identifier}"]
          when: status is published
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
        - $ref: "#/components/parameters/Identifier"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Entry updated.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Entry"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing required content permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Entry or collection not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [Content]
      summary: Soft-delete a content entry
      description: Requires `content.delete` on `content:{collection}:{identifier}`.
      x-required-permission:
        effect: allow
        actions: [content.delete]
        resources: ["content:{collection}:{identifier}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Collection"
        - $ref: "#/components/parameters/Identifier"
      responses:
        "200":
          description: Entry soft-deleted.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Ok"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `content.delete` permission for the entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Entry or collection not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media:
    get:
      tags: [Media]
      summary: List media files
      description: Without a token, only public files are returned. With `media.read` on `media:*`, private files are included.
      x-required-permission:
        effect: allow
        actions: [media.read]
        resources: ["media:*"]
      security:
        - bearerAuth: []
        - {}
      parameters:
        - name: q
          in: query
          schema:
            type: string
        - name: category
          in: query
          schema:
            type: string
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Media file list.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/MediaItem"
                  meta:
                    allOf:
                      - $ref: "#/components/schemas/ListMeta"
                      - type: object
                        properties:
                          categories:
                            type: array
                        items:
                          type: string
        "401":
          description: Invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.read` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [Media]
      summary: Upload media
      description: Requires `media.upload` on `media:*`, or `media:category:{category}` when assigning a category.
      x-required-permission:
        effect: allow
        actions: [media.upload]
        resources: ["media:*", "media:category:{category}"]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [media]
              properties:
                media:
                  type: array
                  items:
                    type: string
                    format: binary
                category:
                  type: string
      responses:
        "201":
          description: Uploaded media items.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/MediaItem"
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.upload` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Upload or validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/categories:
    post:
      tags: [Media]
      summary: Create a media category
      description: Requires `media.update` on `media:*`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "201":
          description: Category created.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [name]
                    properties:
                      name:
                        type: string
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/categories/{category}:
    put:
      tags: [Media]
      summary: Rename a media category
      description: Requires `media.update` on `media:category:{category}`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:category:{category}"]
      security:
        - bearerAuth: []
      parameters:
        - name: category
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Category renamed.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [name]
                    properties:
                      name:
                        type: string
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission for the category.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Media]
      summary: Rename a media category
      description: Same behavior and permissions as `PUT /media/categories/{category}`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:category:{category}"]
      security:
        - bearerAuth: []
      parameters:
        - name: category
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
      responses:
        "200":
          description: Category renamed.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: object
                    required: [name]
                    properties:
                      name:
                        type: string
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission for the category.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [Media]
      summary: Delete a media category
      description: Files assigned to the category are not deleted; their category is cleared. Requires `media.update` on `media:category:{category}`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:category:{category}"]
      security:
        - bearerAuth: []
      parameters:
        - name: category
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Category deleted.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    $ref: "#/components/schemas/Ok"
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission for the category.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/{filename}/category:
    put:
      tags: [Media]
      summary: Assign a media category
      description: Requires `media.update` on `media:{filename}`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:{filename}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category]
              properties:
                category:
                  type: string
                  description: Empty string clears the category.
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission for the file.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Media]
      summary: Assign a media category
      description: Same behavior and permissions as `PUT /media/{filename}/category`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:{filename}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category]
              properties:
                category:
                  type: string
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
                  meta:
                    type: object
                    properties:
                      categories:
                        type: array
                        items:
                          type: string
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission for the file.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/{filename}/meta:
    put:
      tags: [Media]
      summary: Update media metadata
      description: Updates `alt` and `title`. Requires `media.update` on `media:*`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                alt:
                  type: string
                title:
                  type: string
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Media]
      summary: Update media metadata
      description: Same behavior and permissions as `PUT /media/{filename}/meta`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                alt:
                  type: string
                title:
                  type: string
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/{filename}/visibility:
    put:
      tags: [Media]
      summary: Update media visibility
      description: Sets visibility to `public` or `private`. Requires `media.update` on `media:*`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [visibility]
              properties:
                visibility:
                  type: string
                  enum: [public, private]
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Media]
      summary: Update media visibility
      description: Same behavior and permissions as `PUT /media/{filename}/visibility`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [visibility]
              properties:
                visibility:
                  type: string
                  enum: [public, private]
      responses:
        "200":
          description: Updated media item.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/MediaItem"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Media file not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/bulk-visibility:
    put:
      tags: [Media]
      summary: Update media visibility in bulk
      description: Sets visibility for multiple files. Requires `media.update` on `media:*`.
      x-required-permission:
        effect: allow
        actions: [media.update]
        resources: ["media:*"]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [files, visibility]
              properties:
                files:
                  type: array
                  items:
                    type: string
                visibility:
                  type: string
                  enum: [public, private]
      responses:
        "200":
          description: Updated media items.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/MediaItem"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.update` permission.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "422":
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /media/{filename}:
    delete:
      tags: [Media]
      summary: Delete a media file
      description: Requires `media.delete` on `media:{filename}`.
      x-required-permission:
        effect: allow
        actions: [media.delete]
        resources: ["media:{filename}"]
      security:
        - bearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Filename"
      responses:
        "200":
          description: File deleted.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    $ref: "#/components/schemas/Ok"
        "401":
          description: Missing or invalid bearer token.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "403":
          description: Token missing `media.delete` permission for the file.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
