{
  "openapi": "3.1.0",
  "info": {
    "title": "FileMv public API",
    "version": "1",
    "description": "Public API reference: https://filemv.com/developers"
  },
  "servers": [
    {
      "url": "https://filemv.com"
    }
  ],
  "paths": {
    "/api/files": {
      "get": {
        "description": "Returns the direct children of the requested folder, or the user's root items when parentId is omitted. Folder rows carry a recursive size rollup; file rows carry their own size + metadata. The optional `?sort=` parameter (issue #460) selects the result ordering — see the parameter doc for the accepted vocabulary. Unknown values raise 400. The optional `?q=` parameter switches to a folder-independent file search: when non-blank, parentId is ignored and the response contains only the caller's own non-deleted files (across every folder) whose name contains `q` case-insensitively.",
        "operationId": "getFiles",
        "parameters": [
          {
            "description": "UUID of the parent folder. Omit (or null) to list root items. Ignored when `q` is provided.",
            "in": "query",
            "name": "parentId",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Page size. Server-side maximum applies.",
            "in": "query",
            "name": "limit",
            "required": false,
            "schema": {
              "default": 20,
              "format": "int32",
              "type": "integer"
            }
          },
          {
            "description": "1-based page number.",
            "in": "query",
            "name": "page",
            "required": false,
            "schema": {
              "default": 1,
              "format": "int32",
              "type": "integer"
            }
          },
          {
            "description": "Result ordering. Accepted values: `type,name` (default — folder-first-then-alpha, preserves historical behaviour); `name` / `-name` (ascending / descending by name, mixed folders + files); `createdAt` / `-createdAt` (by creation timestamp, mixed); `size` / `-size` (FILES only — folders are surfaced first by name, files after by size). Unknown values raise 400.",
            "in": "query",
            "name": "sort",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional folder-independent file-name search term. When non-blank, `parentId` is ignored and the response lists only the caller's own non-deleted files (across all folders) whose name contains this value case-insensitively. Blank / absent keeps the parentId-scoped folder listing.",
            "in": "query",
            "name": "q",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileListResponse"
                }
              }
            },
            "description": "Paginated list of file-system items"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileListResponse"
                }
              }
            },
            "description": "Unsupported `sort` value"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileListResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileListResponse"
                }
              }
            },
            "description": "parentId is unknown or owned by a different user"
          }
        },
        "summary": "List direct children of a folder (or root items), or search files across all folders",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/archive": {
      "post": {
        "description": "Verifies ownership server-side for every id in the request and builds the archive. Returns a one-shot download URL for the ZIP on success.",
        "operationId": "createArchive",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkArchiveRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Manifest accepted — body carries the archive download URL"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Validation failed (empty / oversize fileIds list)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "One or more ids are unknown, purged, or owned by a different user"
          },
          "413": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Selection exceeds the per-archive size cap (storage.archive.max-bytes) — the uniform error envelope with code=archive_size_exceeded, plus actual_bytes / cap_bytes"
          },
          "502": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Storage worker rejected the archive request for a reason other than the size cap"
          }
        },
        "summary": "Create a one-shot ZIP for a selection of caller-owned files",
        "tags": [
          "Drive — bulk archive"
        ]
      }
    },
    "/api/files/folders": {
      "post": {
        "description": "Creates a folder at the root (parentId omitted) or under an existing folder owned by the caller.",
        "operationId": "createFolder",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFolderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Folder created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Validation failed (blank name)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "parentId is unknown, owned by a different user, or refers to a FILE"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "A sibling with the same name already exists in the destination"
          }
        },
        "summary": "Create a new folder under the caller's tree",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/move": {
      "patch": {
        "description": "Always returns 200 on a well-formed request; per-item outcomes are partitioned into 'moved' and 'failed'. Malformed input (empty ids, no destination) still returns 400.",
        "operationId": "moveBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveBatchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MoveBatchResponse"
                }
              }
            },
            "description": "Per-item success / failure breakdown"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MoveBatchResponse"
                }
              }
            },
            "description": "Validation failed — empty ids, or neither parentId nor moveToRoot supplied"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MoveBatchResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          }
        },
        "summary": "Move several items to the same destination in one request",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/share-multi": {
      "post": {
        "description": "Requires 2..50 distinct ids, all owned, not purged, and non-anonymous. The whole call is rejected on the first failed id — no partial group is committed.",
        "operationId": "mintMultiShare",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MintMultiShareRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "Shared token + frontend URL + final file count"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (size out of [2,50], duplicate ids, invalid shareMode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own one or more files"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "One or more files not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultiShareLinkResponse"
                }
              }
            },
            "description": "One or more files are anonymous"
          }
        },
        "summary": "Mint a single share token spanning multiple caller-owned files",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/trash": {
      "get": {
        "description": "Newest-trashed-first. When a folder is trashed each descendant file lands here as its own row. Folder rows are listed only with `includeFolders=true`, and then only the folders trashed directly (a sub-folder trashed together with its parent comes back with the parent's restore).",
        "operationId": "listTrash",
        "parameters": [
          {
            "description": "Also list the folders the caller trashed directly (type `folder`, `size` null). Default `false`: files only.",
            "in": "query",
            "name": "includeFolders",
            "required": false,
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/FileSystemItemDto"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Trashed rows, newest-trashed-first"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/FileSystemItemDto"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          }
        },
        "summary": "List soft-deleted files owned by the caller",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/visibility": {
      "patch": {
        "description": "Folder ids are accepted and expand to their direct file children (depth-1). Per-item failures are surfaced inline so a partially-failed batch never aborts the whole call. Returns 200 when every item succeeds and 207 Multi-Status when at least one item fails. Inline per-id codes: 404 unknown, 409 trashed-or-anonymous, 410 purged.",
        "operationId": "setVisibility",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VisibilityBatchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisibilityBatchResponse"
                }
              }
            },
            "description": "Every item succeeded — `updated` populated, `failed` empty"
          },
          "207": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisibilityBatchResponse"
                }
              }
            },
            "description": "Partial failure — `failed` is non-empty (per-id 404 / 409 / 410); `updated` holds the items that did succeed"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisibilityBatchResponse"
                }
              }
            },
            "description": "Validation failed (empty ids, invalid mode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VisibilityBatchResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          }
        },
        "summary": "Toggle PUBLIC / PRIVATE visibility for a batch of items",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/download-url": {
      "get": {
        "description": "Inline=true returns a URL with Content-Disposition: inline (browser preview); inline=false returns an attachment URL. A file the caller owns that is quarantined (under takedown) is still served to its owner, from the quarantine store.",
        "operationId": "getDownloadUrl",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "true to request an inline-disposition URL (preview); false (default) yields an attachment URL.",
            "in": "query",
            "name": "inline",
            "required": false,
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            },
            "description": "Body carries the signed download URL"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            },
            "description": "File not found"
          },
          "410": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DownloadUrlResponse"
                }
              }
            },
            "description": "file_purged — the file's bytes were purged (the row is kept); no URL is minted"
          }
        },
        "summary": "Get a signed download URL for a caller-owned file",
        "tags": [
          "Files — download"
        ]
      }
    },
    "/api/files/{fileId}/share": {
      "post": {
        "description": "Idempotent — if an active share exists, returns it instead of minting a new one. On an existing share only the fields present in the body are applied; an omitted field (mode, expiresInDays, notify flags, password) keeps its current value. expiresInDays=0 clears the expiry. An already-lapsed expiry is dropped when no new one is sent.",
        "operationId": "mintShare_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MintShareRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Active share token + frontend share path"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (invalid shareMode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File not found"
          }
        },
        "summary": "Mint or return the active share for a caller-owned file",
        "tags": [
          "File shares"
        ]
      },
      "patch": {
        "description": "Re-issues the same share token under a different mode. The token URL stays stable, only its visibility changes.",
        "operationId": "updateShareMode_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateShareModeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (missing/invalid shareMode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File or active share not found"
          }
        },
        "summary": "Update an existing share's mode without rotating its token",
        "tags": [
          "File shares"
        ]
      },
      "delete": {
        "description": "Idempotent — returns 204 whether or not a share token existed for the caller's file. The 404 is unrelated to the share token: it is returned only when the file itself does not exist.",
        "operationId": "revokeShare_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Share revoked (or no-op if none existed)"
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "Caller does not own the file"
          },
          "404": {
            "description": "File not found (the file itself is absent, not the share token)"
          }
        },
        "summary": "Revoke a file's active share",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/share/expiry": {
      "patch": {
        "description": "Sets or clears the share-link expiry. expiresInDays=0 or null removes the expiry. Applied to all rows sharing the same token (multi-file share support).",
        "operationId": "updateShareExpiry_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareExpiryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with new expiry"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (expiresInDays out of [0,365] or exceeds configured max-days)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Update an existing share's expiry without rotating its token",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/share/notify": {
      "patch": {
        "description": "Sets or clears the notify-on-download flag on an existing share without rotating its token. Applied to all rows sharing the same token (multi-file share support). notifyOnDownload=true enables owner email notifications on recipient download.",
        "operationId": "updateShareNotify_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareNotifyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new notifyOwnerOnDownload flag"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (notifyOnDownload is null)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Enable or disable download-notification emails for a file share",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/share/password": {
      "patch": {
        "description": "Updates the BCrypt-hashed password on an existing share without rotating its token. Applied to all rows sharing the same token (multi-file share support). Pass password=null to clear the password (remove protection). The raw password is never logged or returned.",
        "operationId": "updateSharePassword_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SharePasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new passwordProtected flag"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (password shorter than 4 chars or longer than 128 chars)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Set, change, or clear the password on an existing file share",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/share/slug": {
      "patch": {
        "description": "Sets or clears a custom vanity short link for the share. Available on every plan. Pass slug=null to revert to the auto-generated short link. The slug must be 3–30 chars, lowercase alphanumeric/hyphens, no leading/trailing/consecutive hyphens, and must not be a reserved or profane term.",
        "operationId": "setVanitySlug_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SlugRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new shortUrl"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Slug fails format, reserved-word, or profanity validation"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "File not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share, or slug is already taken by another share"
          }
        },
        "summary": "Set, change, or clear the custom vanity slug on an existing file share",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{fileId}/share/unlock-events": {
      "get": {
        "description": "Owner-only. Returns up to 20 recent unlock attempts (timestamp + result), the 24-hour failed-attempt count, and a brute-force alert flag. IP hashes are never exposed. Returns an empty view when no share is active or the share has no password-attempt history (#1225).",
        "operationId": "getUnlockEvents_1",
        "parameters": [
          {
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Audit view"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Caller does not own the file"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "File not found"
          }
        },
        "summary": "Get unlock-attempt audit log for a file share",
        "tags": [
          "File shares"
        ]
      }
    },
    "/api/files/{id}": {
      "patch": {
        "description": "Body must supply at least one mutation — a move target (parentId or moveToRoot=true) or a non-null name. Move and rename can be combined in a single PATCH and are applied atomically.",
        "operationId": "updateFile",
        "parameters": [
          {
            "description": "UUID of the item to update",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateFileRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "The updated item"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Validation failed (no mutation supplied, name too long)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Item or destination parent is unknown / owned by another user"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Name collision in the destination, or move would create a folder cycle"
          }
        },
        "summary": "Move and/or rename a file-system item",
        "tags": [
          "Drive (file system)"
        ]
      },
      "delete": {
        "description": "Folders are soft-deleted along with every descendant. Items can be restored via POST /{id}/restore until the TrashCleanupService sweeps them after the retention window.",
        "operationId": "deleteFile",
        "parameters": [
          {
            "description": "UUID of the item to soft-delete",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Item soft-deleted"
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "description": "Item is unknown or owned by a different user"
          }
        },
        "summary": "Soft-delete an item (move it to Trash)",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/{id}/path": {
      "get": {
        "description": "Used by the Drive UI to render breadcrumbs when a user deep-links into a folder. The returned list is ordered root-first and ends with the item itself.",
        "operationId": "getPath",
        "parameters": [
          {
            "description": "UUID of the item whose ancestors should be returned",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/FileSystemItemDto"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Ordered ancestor list, ending with the item itself"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/FileSystemItemDto"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/FileSystemItemDto"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Item is unknown or owned by a different user"
          }
        },
        "summary": "Return the ancestor chain of an item",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/{id}/permanent": {
      "delete": {
        "description": "Deletes the stored file and its metadata for good. Idempotent — calling on an already-purged id still returns 204. The item must be in Trash first; calling on an alive row returns 409.",
        "operationId": "permanentDelete",
        "parameters": [
          {
            "description": "UUID of the soft-deleted item to permanently delete",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Item permanently deleted (or already purged)"
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "description": "Item is unknown or owned by a different user"
          },
          "409": {
            "description": "Item exists but is not soft-deleted — caller must soft-delete first"
          }
        },
        "summary": "Permanently delete a soft-deleted item",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/files/{id}/restore": {
      "post": {
        "description": "If the original parent folder is itself in Trash the item is restored to the Drive root. Restoring a folder also restores every descendant that was trashed together with it (same delete); descendants trashed separately before the folder stay in Trash.",
        "operationId": "restoreFile",
        "parameters": [
          {
            "description": "UUID of the soft-deleted item to restore",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "The restored item"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "Item is unknown, owned by a different user, or not in Trash"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FileSystemItemDto"
                }
              }
            },
            "description": "A name collision in the restore destination blocks the restore"
          }
        },
        "summary": "Restore a soft-deleted item from Trash",
        "tags": [
          "Drive (file system)"
        ]
      }
    },
    "/api/folders/{folderId}/share": {
      "post": {
        "description": "Idempotent — returns the existing active share if one is present. On an existing share only the fields present in the body are applied; an omitted field (mode, expiresInDays, notify flags, password) keeps its current value. expiresInDays=0 clears the expiry. An already-lapsed expiry is dropped when no new one is sent.",
        "operationId": "mintShare",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MintShareRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Active share token + frontend share path"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (invalid shareMode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          }
        },
        "summary": "Mint or return the active share for a caller-owned folder",
        "tags": [
          "Folder shares"
        ]
      },
      "patch": {
        "description": "Re-issues the same share token under a different mode. The token URL stays stable, only its visibility changes.",
        "operationId": "updateShareMode",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateShareModeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (missing/invalid shareMode)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder or active share not found"
          }
        },
        "summary": "Update a folder share's mode without rotating its token",
        "tags": [
          "Folder shares"
        ]
      },
      "delete": {
        "description": "Idempotent — returns 204 whether or not a share token existed for the caller's folder. The 404 is unrelated to the share token: it is returned only when the folder itself does not exist.",
        "operationId": "revokeShare",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Share revoked (or no-op if none existed)"
          },
          "401": {
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "description": "Caller does not own the folder"
          },
          "404": {
            "description": "Folder not found (the folder itself is absent, not the share token)"
          }
        },
        "summary": "Revoke a folder's active share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/expiry": {
      "patch": {
        "description": "Sets or clears the share-link expiry. expiresInDays=0 or null removes the expiry. Requires an active folder share.",
        "operationId": "updateShareExpiry",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareExpiryRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with new expiry"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (expiresInDays out of [0,365] or exceeds configured max-days)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Update a folder share's expiry without rotating its token",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/notify": {
      "patch": {
        "description": "Sets or clears the notify-on-download flag on an existing folder share without rotating its token. notifyOnDownload=true enables owner email notifications on recipient download.",
        "operationId": "updateShareNotify",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShareNotifyRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new notifyOwnerOnDownload flag"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (notifyOnDownload is null)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Enable or disable download-notification emails for a folder share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/password": {
      "patch": {
        "description": "Updates the BCrypt-hashed password on an existing folder share without rotating its token. Pass password=null to clear the password (remove protection). The raw password is never logged or returned.",
        "operationId": "updateSharePassword",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SharePasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new passwordProtected flag"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (password shorter than 4 chars or longer than 128 chars)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Set, change, or clear the password on an existing folder share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/proofing": {
      "patch": {
        "description": "Sets or clears the proofing-enabled flag on an existing folder share without rotating its token. proofingEnabled=true lets emailed recipients star/select files and leave notes.",
        "operationId": "updateShareProofing",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateShareProofingRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Validation failed (proofingEnabled is null)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share — mint one first via POST"
          }
        },
        "summary": "Enable or disable client-delivery proofing for a folder share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/slug": {
      "patch": {
        "description": "Sets or clears a custom vanity short link for the folder share. Available on every plan. Pass slug=null to revert to the auto-generated short link. The slug must be 3–30 chars, lowercase alphanumeric/hyphens, no leading/trailing/consecutive hyphens, and must not be a reserved or profane term.",
        "operationId": "setVanitySlug",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SlugRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Updated share descriptor with the new shortUrl"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Slug fails format, reserved-word, or profanity validation"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "Folder not found"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareLinkResponse"
                }
              }
            },
            "description": "No active share, or slug is already taken by another share"
          }
        },
        "summary": "Set, change, or clear the custom vanity slug on an existing folder share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/folders/{folderId}/share/unlock-events": {
      "get": {
        "description": "Owner-only. Returns up to 20 recent unlock attempts (timestamp + result), the 24-hour failed-attempt count, and a brute-force alert flag. IP hashes are never exposed. Returns an empty view when no share is active or the share has no password-attempt history (#1225).",
        "operationId": "getUnlockEvents",
        "parameters": [
          {
            "in": "path",
            "name": "folderId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Audit view"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Caller does not own the folder"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareUnlockAuditResponse"
                }
              }
            },
            "description": "Folder not found"
          }
        },
        "summary": "Get unlock-attempt audit log for a folder share",
        "tags": [
          "Folder shares"
        ]
      }
    },
    "/api/me/api-keys": {
      "get": {
        "description": "Lists the caller's API keys, newest first, revoked ones included. Rows carry the display prefix and lifecycle metadata, never the raw key. `lastUsedAt` is updated at most once a minute.",
        "operationId": "list_1",
        "responses": {
          "200": {
            "content": {
              "*/*": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyRow"
                  },
                  "type": "array"
                }
              }
            },
            "description": "The caller's keys"
          },
          "401": {
            "content": {
              "*/*": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ApiKeyRow"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Not authenticated (`authentication_required`)"
          }
        },
        "summary": "List API keys",
        "tags": [
          "API keys"
        ]
      },
      "post": {
        "description": "Mints a new personal API key. The raw key is returned in `rawKey` exactly once: store it immediately, it can never be retrieved again (only a keyed hash is kept). The body and its `label` are optional.",
        "operationId": "mint",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyMintRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMintResponse"
                }
              }
            },
            "description": "Key created — `rawKey` is shown only in this response"
          },
          "400": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMintResponse"
                }
              }
            },
            "description": "`label` longer than 80 characters (`validation_failed`)"
          },
          "401": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMintResponse"
                }
              }
            },
            "description": "Not authenticated (`authentication_required`)"
          },
          "409": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyMintResponse"
                }
              }
            },
            "description": "The user already has 10 active keys (`api_key_limit_reached`)"
          }
        },
        "summary": "Create an API key",
        "tags": [
          "API keys"
        ]
      }
    },
    "/api/me/api-keys/{id}": {
      "delete": {
        "description": "Revokes one of the caller's keys. Requests carrying it are rejected from then on. Revoking an already revoked key is a no-op.",
        "operationId": "revoke_3",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "description": "Not authenticated (`authentication_required`)"
          },
          "404": {
            "description": "No such key for the caller (`api_key_not_found`)"
          }
        },
        "summary": "Revoke an API key",
        "tags": [
          "API keys"
        ]
      }
    },
    "/api/public/share/{token}": {
      "get": {
        "description": "Public, unauthenticated read. The token may be a file share's or a folder share's token, or a custom slug. For folder shares the optional folderId query parameter narrows the response to that subfolder's direct contents under the share's subtree. When the share is password-protected and no valid X-Share-Unlock token is provided, returns 200 with passwordRequired=true and an empty content envelope.",
        "operationId": "getShare",
        "parameters": [
          {
            "description": "Recipient-facing share token returned by the share-mint endpoints",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional subfolder id to navigate into on a folder share. Must live inside the share's subtree.",
            "in": "query",
            "name": "folderId",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional per-recipient tracking token from an emailed share link (#1214 A2). Best-effort: unknown/foreign values are silently ignored.",
            "in": "query",
            "name": "r",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicShareResponse"
                }
              }
            },
            "description": "Share payload (full content) OR locked stub (passwordRequired=true when password-protected and no valid unlock token)"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicShareResponse"
                }
              }
            },
            "description": "folderId refers to a non-folder item on a folder share"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicShareResponse"
                }
              }
            },
            "description": "No share matches the token, or folderId is outside the share's subtree"
          },
          "410": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicShareResponse"
                }
              }
            },
            "description": "Every file behind this token has been deleted or has expired, or the share link's expiry has passed (#1214 B)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublicShareResponse"
                }
              }
            },
            "description": "Too many resolves from this IP — Retry-After header indicates the backoff window (#591)"
          }
        },
        "summary": "Resolve a share token to its public payload",
        "tags": [
          "Public share"
        ]
      }
    },
    "/api/public/share/{token}/archive": {
      "get": {
        "description": "Builds a manifest of every live FILE descendant under the requested folder (root by default) and redirects the recipient to a one-shot, short-lived streaming-ZIP URL. File / multi-file shares are not eligible; they fall back to the per-file client-side fan-out.",
        "operationId": "getShareArchive",
        "parameters": [
          {
            "description": "Recipient-facing share token — must resolve to a folder share",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional subfolder id (under the share's subtree) to scope the archive to. Defaults to the share root.",
            "in": "query",
            "name": "folderId",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Unlock token proving the caller supplied the correct password (header form). Takes precedence over ?u= when both are present.",
            "in": "header",
            "name": "X-Share-Unlock",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Unlock token proving the caller supplied the correct password (query-param form). Used for browser-navigation downloads where a custom header cannot be set.",
            "in": "query",
            "name": "u",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Redirect to the one-shot streaming-ZIP URL"
          },
          "400": {
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Share is not a folder share, or folderId refers to a non-folder item, or the manifest fails validation"
          },
          "404": {
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "Unknown share token, or folderId is outside the share's subtree"
          },
          "413": {
            "content": {
              "*/*": {
                "schema": {
                  "type": "object"
                }
              }
            },
            "description": "The folder exceeds the per-archive size cap; the uniform error envelope with code=archive_size_exceeded, plus actual_bytes / cap_bytes (E.10 / #187, #630)"
          }
        },
        "summary": "Stream a folder share as a single ZIP archive",
        "tags": [
          "Public share"
        ]
      }
    },
    "/api/public/share/{token}/files/{fileId}/urls": {
      "get": {
        "description": "Long media playback outlives the 10-minute download-token TTL; the player calls this to swap in fresh tokenised URLs without re-resolving the whole share (which would record a share-view event). Applies the same expiry / password / membership gates as the share resolution itself. Records NO view/open analytics.",
        "operationId": "refreshFileUrls",
        "parameters": [
          {
            "description": "Recipient-facing share token or vanity slug",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Id of the file whose URLs should be re-minted; must belong to the share",
            "in": "path",
            "name": "fileId",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional per-recipient tracking token — preserved as an attribution claim in the minted download token. Foreign/unknown values are silently dropped.",
            "in": "query",
            "name": "r",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareFileUrlsResponse"
                }
              }
            },
            "description": "Fresh inline + attachment URLs"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareFileUrlsResponse"
                }
              }
            },
            "description": "Share is password-protected and no valid X-Share-Unlock token was supplied"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareFileUrlsResponse"
                }
              }
            },
            "description": "No share matches the token, or the file does not belong to the share"
          },
          "410": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareFileUrlsResponse"
                }
              }
            },
            "description": "The share's link expiry has passed"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShareFileUrlsResponse"
                }
              }
            },
            "description": "Too many refreshes from this IP — Retry-After header indicates the backoff window (#588)"
          }
        },
        "summary": "Mint fresh download URLs for one file of a share (media token refresh)",
        "tags": [
          "Public share"
        ]
      }
    },
    "/api/public/share/{token}/unlock": {
      "post": {
        "description": "Verifies the supplied password against the share's BCrypt hash. On success returns a signed unlock token (and the full share payload) that the client should send as X-Share-Unlock on subsequent GET requests. Wrong passwords and rate-limit exhaustion are surfaced as 401 / 429.",
        "operationId": "unlockShare",
        "parameters": [
          {
            "description": "The share token to unlock",
            "in": "path",
            "name": "token",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UnlockShareRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "Password correct — body carries the unlock token and the full share payload"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "The share is not password-protected"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "Incorrect password (action=share_password_invalid)"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "Share token not found"
          },
          "410": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "Share has expired"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UnlockShareResponse"
                }
              }
            },
            "description": "Too many failed attempts — Retry-After header indicates backoff window"
          }
        },
        "summary": "Verify a share password and obtain a short-lived unlock token",
        "tags": [
          "Public share"
        ]
      }
    },
    "/api/public/uploads": {
      "post": {
        "description": "Reserves an upload session for an unauthenticated uploader, mints a share token, and applies the configured retention. A missing/rejected captcha surfaces as 400 before any session is reserved. publicListing=true creates the share PUBLIC (listed, indexable by search engines); absent or false keeps the link-only SHARED default.",
        "operationId": "createUploadSession_1",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAnonymousUploadRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadResponse"
                }
              }
            },
            "description": "Session created — body carries the upload URL and the share token"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadResponse"
                }
              }
            },
            "description": "Validation failed (code validation_failed: blank fileName/contentType, non-positive size) or captcha missing/rejected (code captcha_failed, message \"Captcha verification failed\")"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadResponse"
                }
              }
            },
            "description": "Per-IP anonymous-upload rate-limit exceeded"
          }
        },
        "summary": "Create an anonymous upload session",
        "tags": [
          "Anonymous uploads"
        ]
      }
    },
    "/api/public/uploads/batch": {
      "post": {
        "description": "Multi-file twin of POST /api/public/uploads. Verifies the captcha token ONCE, consumes ONE per-IP rate-limit token for the whole batch, validates every file's size cap before reserving anything, then reserves one upload session per file (own share token and short link each) and returns them in request order. Batch size is capped by anonymousMaxFilesPerBatch (/api/public/config). publicListing=true creates every share PUBLIC (listed, indexable by search engines); absent or false keeps the link-only SHARED default.",
        "operationId": "createUploadBatch",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAnonymousUploadBatchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadBatchResponse"
                }
              }
            },
            "description": "Sessions created — body carries one single-file response per file, in request order"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadBatchResponse"
                }
              }
            },
            "description": "Validation failed (empty list, blank fileName/contentType, non-positive size, more files than anonymousMaxFilesPerBatch) or captcha missing/rejected (code captcha_failed)"
          },
          "413": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadBatchResponse"
                }
              }
            },
            "description": "At least one file exceeds the anonymous per-file size cap — nothing was reserved"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateAnonymousUploadBatchResponse"
                }
              }
            },
            "description": "Per-IP anonymous-upload rate-limit exceeded"
          }
        },
        "summary": "Create anonymous upload sessions for a batch of files",
        "tags": [
          "Anonymous uploads"
        ]
      }
    },
    "/api/share/slug-available": {
      "get": {
        "description": "Returns {available:true} when the slug is free. Returns {available:false, reason:...} for taken/reserved/profane/invalid. Advisory only — does not reserve the slug.",
        "operationId": "available",
        "parameters": [
          {
            "in": "query",
            "name": "slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/SlugAvailabilityResponse"
                }
              }
            },
            "description": "Availability result"
          },
          "401": {
            "content": {
              "*/*": {
                "schema": {
                  "$ref": "#/components/schemas/SlugAvailabilityResponse"
                }
              }
            },
            "description": "Missing or invalid JWT"
          }
        },
        "summary": "Check vanity-slug availability",
        "tags": [
          "Shares"
        ]
      }
    },
    "/api/uploads": {
      "post": {
        "description": "Reserves an upload session for the authenticated user and returns the uploadUrl to PUT the file's bytes to. When parentFolderId is supplied, the folder is validated to be owned by the caller before any session is reserved. When autoShare is set, a link-only (SHARED) share token and its short link are minted up front and returned with the reservation. Adding publicListing=true to autoShare makes that share PUBLIC (listed: the share page may be indexed by search engines) instead of SHARED.",
        "operationId": "createUploadSession",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUploadSessionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "Session created — body carries sessionId and uploadUrl, plus the share fields when autoShare was set"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "Validation failed (blank fileName/contentType, non-positive size)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "parentFolderId is unknown, owned by another user, or refers to a FILE"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "Upload would exceed the user's storage quota (#1080)"
          },
          "413": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadSessionApiResponse"
                }
              }
            },
            "description": "Declared fileSizeBytes exceeds the per-file limit of the caller's plan (freeUserMaxBytes / paidUserMaxBytes in GET /api/public/config) — code=payload_too_large"
          }
        },
        "summary": "Create an upload session for an authenticated user",
        "tags": [
          "Uploads"
        ]
      }
    },
    "/api/uploads/{sessionId}/status": {
      "get": {
        "description": "Returns the session id, its state (PENDING / UPLOADING / COMPLETE / FAILED), and — once complete — the resulting fileId + fileName. Ownership is enforced: a session may only be read by the user who created it.",
        "operationId": "getUploadStatus",
        "parameters": [
          {
            "description": "Upload session UUID returned by POST /api/uploads",
            "in": "path",
            "name": "sessionId",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Current session status"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Malformed sessionId"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Missing or invalid Bearer token"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Session is owned by a different user"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Unknown sessionId"
          },
          "502": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionStatusResponse"
                }
              }
            },
            "description": "Storage worker rejected this service's own credential, or answered an unresolvable status"
          }
        },
        "summary": "Look up the current status of an upload session",
        "tags": [
          "Uploads"
        ]
      }
    },
    "/api/users/me/webhooks": {
      "get": {
        "description": "Returns all enabled and disabled webhook endpoints for the calling user. The signingSecretMasked field exposes only the last 4 characters of the 64-char secret.",
        "operationId": "list",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/WebhookResponse"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Webhook list (may be empty)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/WebhookResponse"
                  },
                  "type": "array"
                }
              }
            },
            "description": "Missing or invalid authentication"
          }
        },
        "summary": "List all registered webhooks (secret masked)",
        "tags": [
          "Webhooks"
        ]
      },
      "post": {
        "description": "Creates a new webhook endpoint and returns the full HMAC signing secret once. Available on every plan. The URL must pass SSRF validation (400 webhook_url_blocked if not). The per-user limit is enforced (409 webhook_limit_reached if exceeded).",
        "operationId": "create",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Webhook registered — response body contains the one-time full signing secret"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Validation failed (blank URL, URL too long, or URL blocked by SSRF filter)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Missing or invalid authentication"
          },
          "409": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Per-user webhook limit reached (webhook_limit_reached)"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Too many webhook mutations from this user — Retry-After header indicates the backoff window (#592)"
          }
        },
        "summary": "Register a new webhook endpoint",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/users/me/webhooks/{id}": {
      "patch": {
        "description": "Both fields are optional — omitting a field is a no-op for that attribute. A URL change re-runs SSRF validation. Returns 404 if the webhook does not exist or belongs to another user.",
        "operationId": "update_2",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                }
              }
            },
            "description": "Updated webhook (secret masked)"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                }
              }
            },
            "description": "Validation failed (URL too long or URL blocked by SSRF filter)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                }
              }
            },
            "description": "Missing or invalid authentication"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookResponse"
                }
              }
            },
            "description": "Webhook not found or belongs to another user"
          }
        },
        "summary": "Partially update a webhook (url and/or enabled state)",
        "tags": [
          "Webhooks"
        ]
      },
      "delete": {
        "description": "Purges all webhook_delivery rows first, then removes the webhook row. Returns 404 if the webhook does not exist or belongs to another user.",
        "operationId": "delete_1",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Webhook deleted"
          },
          "401": {
            "description": "Missing or invalid authentication"
          },
          "404": {
            "description": "Webhook not found or belongs to another user"
          }
        },
        "summary": "Delete a webhook and its delivery history",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/users/me/webhooks/{id}/rotate-secret": {
      "post": {
        "description": "Generates a new 64-character HMAC signing secret. The old secret is immediately invalidated — any in-flight delivery signed with the old secret will fail verification. The full new secret is returned once; store it securely. Returns 404 if the webhook does not exist or belongs to another user.",
        "operationId": "rotateSecret",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "New signing secret (full, one-time)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Missing or invalid authentication"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Webhook not found or belongs to another user"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSecretResponse"
                }
              }
            },
            "description": "Too many webhook mutations from this user — Retry-After header indicates the backoff window (#592)"
          }
        },
        "summary": "Rotate the HMAC signing secret for a webhook",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/api/users/me/webhooks/{id}/test": {
      "post": {
        "description": "Synchronous test call — no outbox row is persisted and no retry is scheduled. Returns the HTTP outcome immediately. Returns 404 if the webhook does not exist or belongs to another user.",
        "operationId": "sendTest",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                }
              }
            },
            "description": "Test delivery outcome (check delivered field)"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                }
              }
            },
            "description": "Missing or invalid authentication"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                }
              }
            },
            "description": "Webhook not found or belongs to another user"
          },
          "429": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookTestResponse"
                }
              }
            },
            "description": "Too many webhook mutations from this user — Retry-After header indicates the backoff window (#592)"
          }
        },
        "summary": "Send a one-shot test webhook delivery",
        "tags": [
          "Webhooks"
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AnonymousUploadFileDescriptor": {
        "description": "One file of a batch anonymous-upload kickoff. Same fields as the single-file request minus the captcha token, which is carried once by the enclosing batch envelope.",
        "properties": {
          "contentType": {
            "description": "MIME content type as detected by the client. Required.",
            "example": "image/jpeg",
            "minLength": 1,
            "type": "string"
          },
          "fileName": {
            "description": "Display name of the file being uploaded. Required.",
            "example": "nyaralas-2026.jpg",
            "minLength": 1,
            "type": "string"
          },
          "sizeBytes": {
            "description": "Declared file size in bytes. Must be > 0.",
            "example": 524288,
            "exclusiveMinimum": 0,
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "contentType",
          "fileName"
        ],
        "type": "object"
      },
      "ApiKeyMintRequest": {
        "properties": {
          "label": {
            "description": "Optional free-text label (≤ 80 chars) to tell keys apart. Blank means none.",
            "example": "CI server",
            "maxLength": 80,
            "minLength": 0,
            "type": "string"
          }
        },
        "type": "object"
      },
      "ApiKeyMintResponse": {
        "properties": {
          "createdAt": {
            "example": "2026-10-04T09:30:00Z",
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "example": "7f3c2a9e-4b1d-4c8a-9e2f-1a2b3c4d5e6f",
            "type": "string"
          },
          "label": {
            "example": "CI server",
            "type": "string"
          },
          "prefix": {
            "description": "Display prefix used in lists",
            "example": "fmv_EXAMPL…",
            "type": "string"
          },
          "rawKey": {
            "description": "The full key: `fmv_` + 40 URL-safe base64 characters. Returned only once.",
            "example": "fmv_EXAMPLEexampleEXAMPLEexampleEXAMPLEexamp",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ApiKeyRow": {
        "properties": {
          "active": {
            "example": true,
            "type": "boolean"
          },
          "createdAt": {
            "example": "2026-10-04T09:30:00Z",
            "format": "date-time",
            "type": "string"
          },
          "id": {
            "example": "7f3c2a9e-4b1d-4c8a-9e2f-1a2b3c4d5e6f",
            "type": "string"
          },
          "label": {
            "example": "CI server",
            "type": "string"
          },
          "lastUsedAt": {
            "description": "Last successful use; updated at most once a minute. Null if never used.",
            "example": "2026-10-04T10:02:00Z",
            "format": "date-time",
            "type": "string"
          },
          "prefix": {
            "example": "fmv_EXAMPL…",
            "type": "string"
          },
          "revokedAt": {
            "description": "Null while the key is active",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "BulkArchiveRequest": {
        "properties": {
          "fileIds": {
            "description": "Caller-owned file UUIDs to bundle into the ZIP. Bounded [1, 200] — both limits surface as 400.",
            "example": [
              "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
              "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
            ],
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "maxItems": 200,
            "minItems": 1,
            "type": "array"
          }
        },
        "required": [
          "fileIds"
        ],
        "type": "object"
      },
      "CreateAnonymousUploadBatchRequest": {
        "description": "Batch anonymous-upload kickoff payload for POST /api/public/uploads/batch. One captcha token admits every file; the per-IP limiter consumes a single token for the whole batch. Files are reserved in list order and the response mirrors that order. The batch size is capped by filemv.anonymous.max-files-per-batch (surfaced as anonymousMaxFilesPerBatch on /api/public/config).",
        "properties": {
          "captchaToken": {
            "description": "Cloudflare Turnstile token (D.3 / #173), verified ONCE for the whole batch. Required when filemv.captcha.enabled=true; tolerated blank in dev/test.",
            "type": "string"
          },
          "files": {
            "description": "Files to reserve sessions for, in display order. At least one; at most anonymousMaxFilesPerBatch.",
            "items": {
              "$ref": "#/components/schemas/AnonymousUploadFileDescriptor"
            },
            "minItems": 1,
            "type": "array"
          },
          "publicListing": {
            "description": "Optional. When true every file's share is created PUBLIC (listed): the share page may be indexed by search engines and the file is returned by FileMv's own search. Absent or false keeps the link-only SHARED default (unlisted, noindex).",
            "type": "boolean"
          }
        },
        "required": [
          "files"
        ],
        "type": "object"
      },
      "CreateAnonymousUploadBatchResponse": {
        "description": "Response envelope from POST /api/public/uploads/batch — one single-file session response per requested file, in request order.",
        "properties": {
          "files": {
            "description": "Reserved sessions, in the same order as the request's files list.",
            "items": {
              "$ref": "#/components/schemas/CreateAnonymousUploadResponse"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "CreateAnonymousUploadRequest": {
        "description": "Public anonymous-upload kickoff payload for POST /api/public/uploads. The client declares the file name, MIME, and size; server derives the uploader IP, share token, claim token, and hard expiry. No auth required.",
        "properties": {
          "captchaToken": {
            "description": "Cloudflare Turnstile token (D.3 / #173). Required when filemv.captcha.enabled=true; tolerated blank in dev/test.",
            "type": "string"
          },
          "contentType": {
            "description": "MIME content type as detected by the client. Required.",
            "example": "image/png",
            "minLength": 1,
            "type": "string"
          },
          "fileName": {
            "description": "Display name of the file being uploaded. Required.",
            "example": "screenshot.png",
            "minLength": 1,
            "type": "string"
          },
          "publicListing": {
            "description": "Optional. When true the share is created PUBLIC (listed): the share page may be indexed by search engines and the file is returned by FileMv's own search. Absent or false keeps the link-only SHARED default (unlisted, noindex).",
            "type": "boolean"
          },
          "sizeBytes": {
            "description": "Declared file size in bytes. Must be > 0.",
            "example": 524288,
            "exclusiveMinimum": 0,
            "format": "int64",
            "type": "integer"
          }
        },
        "required": [
          "contentType",
          "fileName"
        ],
        "type": "object"
      },
      "CreateAnonymousUploadResponse": {
        "description": "Response envelope from POST /api/public/uploads. Bundles the storage-worker upload-session handle and the recipient-facing share token. An anonymous upload stays anonymous: there is no mechanism by which a later login adopts it.",
        "properties": {
          "directUrl": {
            "description": "Relative, token-free image URL for hot-linking in forum / HTML / Markdown embeds. Only set for image/* uploads (currently the public display-1080 variant route, which 302s to a fresh storage-worker URL on every request and re-checks access gates); null for non-images. Never a signed /files/…?token= URL.",
            "type": [
              "null",
              "string"
            ]
          },
          "fileId": {
            "description": "Canonical file UUID. Distinct from `shareToken` (the public addressing scheme): the share token can be rotated or revoked while `fileId` is stable for the lifetime of the row. Today `fileId == sessionId`.",
            "type": "string"
          },
          "sessionId": {
            "description": "Storage-worker upload-session UUID. Used by the client to PUT the bytes and to poll status.",
            "type": "string"
          },
          "shareToken": {
            "description": "Recipient-facing share token (22-char URL-safe base64). Used in `/share/{shareToken}` URLs the uploader sends to recipients. Distinct from `fileId`: the share token is the public addressing scheme for the file; `fileId` is the canonical internal identity.",
            "type": "string"
          },
          "shortUrl": {
            "description": "Relative short-link path ('/s/<slug>') for this anonymous share (epic #1214 Phase E). Always non-null on a fresh session — the slug is minted atomically alongside the share token.",
            "type": "string"
          },
          "uploadUrl": {
            "description": "Direct storage-worker URL the client PUTs (small files) or POSTs to /files/tus (resumable) for upload.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreateFolderRequest": {
        "properties": {
          "name": {
            "description": "Display name for the new folder. Required. Max 255 chars; must not contain path separators.",
            "example": "Holiday 2025",
            "minLength": 1,
            "type": "string"
          },
          "parentId": {
            "description": "Optional parent folder UUID. Omit to create the folder at the user's Drive root.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "CreateUploadSessionApiResponse": {
        "description": "Response envelope from POST /api/uploads. Always carries the storage-worker upload-session handle; also carries the share token / short link / hot-linkable image URL when the request opted into autoShare.",
        "properties": {
          "directUrl": {
            "description": "Relative, token-free image URL for hot-linking in forum / HTML / Markdown embeds — set only for an autoShare upload of an image/*; null for non-images and for uploads that did not opt in. Never a signed /files/…?token= URL. See DirectImageUrl for the single composition point.",
            "type": [
              "null",
              "string"
            ]
          },
          "fileId": {
            "description": "Canonical file UUID. Today `fileId == sessionId` (the storage-worker mints one UUID for session, file and s3 key) but the two identities are conceptually distinct, so the value is exposed under its own name.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          },
          "sessionId": {
            "description": "Storage-worker upload-session UUID. The client uses this id to PUT the bytes and to poll status.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          },
          "shareToken": {
            "description": "Recipient-facing share token (22-char URL-safe base64) for an autoShare upload; null otherwise. The file is persisted link-only (SHARED, un-listed) when this is set — the token is minted before the bytes move, so `/share/{shareToken}` only resolves once the upload completes.",
            "type": [
              "null",
              "string"
            ]
          },
          "shortUrl": {
            "description": "Relative short-link path ('/s/<slug>') for the autoShare token; null otherwise. Preferred over `/share/{shareToken}` wherever a link is handed to a human.",
            "type": [
              "null",
              "string"
            ]
          },
          "uploadUrl": {
            "description": "Direct storage-worker URL the client PUTs (small files) or POSTs to /files/tus (resumable) for upload.",
            "example": "https://filemv.com/files/2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreateUploadSessionRequest": {
        "description": "Public payload for POST /api/uploads (authenticated upload kickoff). The web-backend forwards this to the storage-worker's POST /internal/sessions, which reserves an upload session and returns a session id + direct upload URL. Server-populated fields (owner, uploader IP, share token, owner type) are hidden from the published schema: the controller overwrites whatever a client sends, and only the storage-worker hop carries them.",
        "properties": {
          "autoShare": {
            "description": "Optional. When true, a link-only (SHARED) share is minted for this upload, and the response carries its token and short link before the bytes move. Defaults to false: the file stays PRIVATE.",
            "type": "boolean"
          },
          "contentType": {
            "description": "MIME content type as detected by the client. Required.",
            "example": "image/jpeg",
            "minLength": 1,
            "type": "string"
          },
          "fileName": {
            "description": "Display name of the file being uploaded. Required.",
            "example": "vacation.jpg",
            "minLength": 1,
            "type": "string"
          },
          "fileSizeBytes": {
            "description": "Declared file size in bytes. Must be > 0.",
            "example": 1048576,
            "exclusiveMinimum": 0,
            "format": "int64",
            "type": "integer"
          },
          "parentFolderId": {
            "description": "Optional Drive folder UUID to upload into. When omitted the file lands at the user's Drive root. Validated server-side against the authenticated user's tree before any session is reserved.",
            "type": "string"
          },
          "publicListing": {
            "description": "Optional, only honoured together with autoShare=true. When true the minted share is PUBLIC (listed) instead of link-only SHARED: the share page may be indexed by search engines and the file is returned by FileMv's own cross-owner search. Ignored when autoShare is not set (the upload stays PRIVATE). Defaults to false.",
            "type": "boolean"
          }
        },
        "required": [
          "contentType",
          "fileName"
        ],
        "type": "object"
      },
      "CreateWebhookRequest": {
        "description": "Body for POST /api/users/me/webhooks — register a new webhook endpoint",
        "properties": {
          "url": {
            "description": "HTTPS delivery endpoint URL. Must pass SSRF validation.",
            "example": "https://example.com/webhook",
            "maxLength": 2048,
            "minLength": 0,
            "type": "string"
          }
        },
        "required": [
          "url"
        ],
        "type": "object"
      },
      "DownloadUrlResponse": {
        "properties": {
          "downloadUrl": {
            "description": "Storage-worker /files/<key> URL. Not pre-signed today — the surface is auth-free and CDN-cacheable; Content-Disposition + Content-Type are resolved server-side from S3 user-metadata (issue #405).",
            "type": "string"
          }
        },
        "type": "object"
      },
      "FileExifPayload": {
        "description": "Whitelisted, display-formatted image metadata — wire DTO for the storage-worker → web-backend EXIF callback (POST /internal/files/{fileId}/exif) and the `exif` block on a share-page file. Group titles and row keys are locale-independent translation keys the frontend localises; values are pre-formatted display strings. Raw EXIF is never forwarded or stored; GPS coordinates are excluded (only gpsPresent flags them).",
        "properties": {
          "gpsPresent": {
            "description": "True when the original file carries a GPS position (#821). Only the fact is exposed — coordinates are never extracted or stored. Absent / null when unknown (images processed before #821).",
            "example": true,
            "type": [
              "boolean",
              "null"
            ]
          },
          "groups": {
            "description": "Expanded-table columns. Empty only when the image has no whitelisted tag but carries a GPS position (gpsPresent=true); an image with neither is never sent.",
            "items": {
              "$ref": "#/components/schemas/Group"
            },
            "type": "array"
          },
          "summary": {
            "description": "Chip-strip values in display order (camera · focal length · aperture · shutter · ISO · resolution · capture date). Already formatted; absent tags are omitted, not blanked. Max 7.",
            "example": [
              "1/500 s",
              "2026. 08. 12.",
              "35 mm",
              "6000×4000",
              "ISO 200",
              "Sony α7 IV",
              "ƒ/2.8"
            ],
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "maxItems": 7,
            "minItems": 0,
            "type": "array"
          }
        },
        "required": [
          "groups",
          "summary"
        ],
        "type": "object"
      },
      "FileListResponse": {
        "description": "Paginated Drive listing for GET /api/files. The `files` array mixes folders and files — clients distinguish by `FileSystemItemDto.type` (`folder` vs `file`). Folders are returned first, then files, in name order within each group.",
        "properties": {
          "files": {
            "description": "File-system items on the requested page — folders first, then files.",
            "items": {
              "$ref": "#/components/schemas/FileSystemItemDto"
            },
            "type": "array"
          },
          "pagination": {
            "$ref": "#/components/schemas/PaginationDto",
            "description": "Pagination envelope (current page, total pages, total items)."
          }
        },
        "type": "object"
      },
      "FileSystemItemDto": {
        "description": "Unified row shape for Drive listings — represents either a folder or a file. Clients MUST branch on `type` before reading metadata-backed fields (size, mimeType, shareToken, shareMode, uploadedAt) — those are null for folders on non-listing paths, and folder `size` is the recursive descendant-size sum on listing paths only.",
        "properties": {
          "commentsCount": {
            "description": "Live (non-deleted) comment count for the file. Populated only on the Drive listing path (GET /api/files), null elsewhere and for folders (#946).",
            "format": "int32",
            "type": "integer"
          },
          "contentKind": {
            "description": "Content discriminator (#697): FILE for an uploaded binary, PASTE for a text paste — the Drive UI shows a paste icon and routes open to the paste editor instead of the preview overlay. Null for folders.",
            "enum": [
              "FILE",
              "PASTE"
            ],
            "type": "string"
          },
          "deletedAt": {
            "description": "Soft-delete timestamp from file_system_items.deleted_at. Populated only on the trash-listing path (GET /api/files/trash); null on every other surface.",
            "format": "date-time",
            "type": "string"
          },
          "downloadCount": {
            "description": "Lifetime download count for the file — every download ever recorded, with no time window. Computed as the live `download_event` rows plus the summarised `download_event_monthly` buckets for rows already trimmed past the event-retention horizon, so it does not decay as raw events age out. Populated only on the Drive listing path (GET /api/files), null elsewhere and for folders (#635).",
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "description": "Item UUID. Stable for the lifetime of the row; safe to use as a client-side key.",
            "type": "string"
          },
          "likesCount": {
            "description": "Like count for the file (denormalised file_metadata.likes_count). 0 for folders.",
            "format": "int32",
            "type": "integer"
          },
          "mimeType": {
            "description": "MIME content type (files only). Null for folders.",
            "type": "string"
          },
          "name": {
            "description": "Display name. Folder names and file basenames; the backend does not enforce uniqueness within a parent.",
            "type": "string"
          },
          "parentId": {
            "description": "Parent folder UUID, or null when the item lives at the user's Drive root.",
            "type": "string"
          },
          "previewUrl": {
            "description": "Pre-signed inline-preview URL (files only). Null for folders.",
            "type": "string"
          },
          "shareMode": {
            "description": "Share visibility mode of the file (e.g. PUBLIC / UNLISTED). Null when not shared / for folders.",
            "type": "string"
          },
          "shareToken": {
            "description": "Recipient-facing share token, when the file is shared. Null otherwise / for folders.",
            "type": "string"
          },
          "size": {
            "description": "Size in bytes. For files this is the file size; for folders it is the recursive sum of descendant FILE sizes on listing paths (null on non-listing paths).",
            "format": "int64",
            "type": "integer"
          },
          "type": {
            "description": "Item kind discriminator. `folder` denotes a Drive folder (no underlying storage object); `file` denotes a stored file with a `file_metadata` row. Clients MUST switch on this before reading metadata fields — folders have null `mimeType`/`shareToken`/`shareMode`/`uploadedAt` and a context-dependent `size` (recursive descendant sum on listing paths, null elsewhere).",
            "enum": [
              "file",
              "folder"
            ],
            "type": "string"
          },
          "uploadedAt": {
            "description": "Upload timestamp (files only). Null for folders.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "Group": {
        "description": "One column of the expanded EXIF table.",
        "properties": {
          "rows": {
            "description": "Key/value pairs in display order, each a two-element [key, value] array. The key is a locale-independent translation key; the value is a formatted display string.",
            "example": [
              [
                "Sony",
                "make"
              ],
              [
                "ILCE-7M4",
                "model"
              ]
            ],
            "items": {
              "items": {
                "type": "string"
              },
              "maxItems": 2,
              "minItems": 2,
              "type": "array"
            },
            "type": "array"
          },
          "title": {
            "description": "Locale-independent group key the frontend localises — camera | exposure | file.",
            "example": "camera",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "rows",
          "title"
        ],
        "type": "object"
      },
      "MintMultiShareRequest": {
        "properties": {
          "expiresInDays": {
            "description": "Optional share-link expiry in days from now, applied to every file in the group. null or 0 = never expires; 1..365 = expire after N days.",
            "example": 7,
            "format": "int32",
            "maximum": 365,
            "minimum": 0,
            "type": "integer"
          },
          "fileIds": {
            "description": "Caller-owned file UUIDs to bundle under one share token. Bounded [2, 50] — single-file shares should go through POST /api/files/{id}/share instead.",
            "example": [
              "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
              "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
            ],
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "maxItems": 50,
            "minItems": 2,
            "type": "array"
          },
          "notifyOnDownload": {
            "description": "When true, the file owner receives an email notification when an emailed recipient downloads any file in this multi-share group (epic #1214 Phase D). Omit or set to false to disable.",
            "example": true,
            "type": "boolean"
          },
          "notifyOnView": {
            "description": "Optional. When true, the file owner receives an email notification when an emailed recipient first opens any file in this multi-share group (issue #1228). Distinct from notifyOnDownload: fires on view.",
            "type": "boolean"
          },
          "notifyPerRecipient": {
            "description": "Optional. When true, switches to per-recipient notification mode (issue #1229): the owner receives one email per recipient download instead of only the first. Requires notifyOnDownload=true to have any visible effect.",
            "type": "boolean"
          },
          "password": {
            "description": "Optional share password (4 characters to 72 bytes of UTF-8), applied to every file in the group. null/omitted = no password; @Size skips null.",
            "maxLength": 72,
            "minLength": 4,
            "type": "string"
          },
          "shareMode": {
            "description": "Optional target share mode (SHARED | PUBLIC). Defaults to SHARED — link-only, unlisted.",
            "enum": [
              "PRIVATE",
              "PUBLIC",
              "SHARED"
            ],
            "example": "SHARED",
            "type": "string"
          }
        },
        "required": [
          "fileIds"
        ],
        "type": "object"
      },
      "MintShareRequest": {
        "properties": {
          "expiresInDays": {
            "description": "Optional share-link expiry in days from now. 0 = never expires; 1..365 = expire after N days. Omitted: never on a new share, unchanged on an existing one (an already-lapsed expiry is dropped).",
            "example": 7,
            "format": "int32",
            "maximum": 365,
            "minimum": 0,
            "type": "integer"
          },
          "notifyOnDownload": {
            "description": "When true, the owner receives an email notification when an emailed recipient downloads this share (epic #1214 Phase D). Omitted: false on a new share, unchanged on an existing one.",
            "example": true,
            "type": "boolean"
          },
          "notifyOnView": {
            "description": "Optional. When true, the owner receives an email notification when an emailed recipient first opens this share link (issue #1228). Distinct from notifyOnDownload: fires on open (view), not download. Omitted: false on a new share, unchanged on an existing one.",
            "type": "boolean"
          },
          "notifyPerRecipient": {
            "description": "Optional. When true, switches to per-recipient notification mode (issue #1229): the owner receives one email per recipient download instead of only the first. Requires notifyOnDownload=true to have any visible effect. Omitted: false on a new share, unchanged on an existing one.",
            "type": "boolean"
          },
          "password": {
            "description": "Optional share password (4 characters to 72 bytes of UTF-8). null/omitted = no password on a new share, unchanged on an existing one (clear it via PATCH …/share/password); empty/1–3 chars or over 72 bytes = 400. @Size skips null.",
            "example": "correct-horse-42",
            "maxLength": 72,
            "minLength": 4,
            "type": "string"
          },
          "shareMode": {
            "description": "Optional target share mode (SHARED | PUBLIC). Omitted: SHARED (link-only, unlisted) on a new share, unchanged on an existing one.",
            "enum": [
              "PRIVATE",
              "PUBLIC",
              "SHARED"
            ],
            "example": "SHARED",
            "type": "string"
          }
        },
        "type": "object"
      },
      "MoveBatchRequest": {
        "properties": {
          "ids": {
            "description": "UUIDs of the file-system items to move. Must contain at least one entry.",
            "example": [
              "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
              "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
            ],
            "items": {
              "type": "string"
            },
            "minItems": 1,
            "type": "array"
          },
          "moveToRoot": {
            "description": "Move every item to the user's Drive root. Mutually exclusive with parentId.",
            "type": "boolean"
          },
          "parentId": {
            "description": "Destination parent folder UUID. Mutually exclusive with moveToRoot.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          }
        },
        "required": [
          "ids"
        ],
        "type": "object"
      },
      "MoveBatchResponse": {
        "properties": {
          "failed": {
            "description": "Items the server refused to move, with the per-item HTTP status and reason.",
            "items": {
              "$ref": "#/components/schemas/MoveFailure"
            },
            "type": "array"
          },
          "moved": {
            "description": "Items successfully relocated, with their up-to-date DTOs.",
            "items": {
              "$ref": "#/components/schemas/FileSystemItemDto"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "MoveFailure": {
        "properties": {
          "id": {
            "description": "Item UUID that the request asked to move.",
            "type": "string"
          },
          "message": {
            "description": "Human-readable reason for the failure.",
            "type": "string"
          },
          "status": {
            "description": "HTTP status the single-item endpoint would have returned for this item (e.g. 404, 409, 400).",
            "format": "int32",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "MultiShareLinkResponse": {
        "properties": {
          "fileCount": {
            "description": "Number of files bundled under the token after server-side validation.",
            "format": "int32",
            "type": "integer"
          },
          "shareMode": {
            "description": "Final share mode the row was committed under (SHARED | PUBLIC).",
            "type": "string"
          },
          "shareToken": {
            "description": "Recipient-facing share token (the literal '/share/<token>' URL segment).",
            "type": "string"
          },
          "shareUrl": {
            "description": "Frontend share path (typically '/share/<token>'). Pre-baked so the caller doesn't have to assemble it.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PaginationDto": {
        "properties": {
          "currentPage": {
            "description": "1-based page number that was returned.",
            "format": "int32",
            "type": "integer"
          },
          "totalItems": {
            "description": "Total number of items across every page.",
            "format": "int32",
            "type": "integer"
          },
          "totalPages": {
            "description": "Total number of pages available given the current page size.",
            "format": "int32",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PortalBranding": {
        "description": "Portal branding inherited from the share owner's branded portal, present only when the owner is paid, has a published, non-taken-down portal with apply-to-shares switched on.",
        "properties": {
          "logoUrl": {
            "description": "Portal logo URL (/api/public/portals/{slug}/logo.png), or null when the portal has no uploaded logo.",
            "type": "string"
          },
          "portalSlug": {
            "description": "Portal slug — builds the 'Portál megtekintése' link to /p/{slug}.",
            "type": "string"
          },
          "portalTitle": {
            "description": "Portal display title.",
            "type": "string"
          },
          "theme": {
            "$ref": "#/components/schemas/PortalTheme",
            "description": "Portal theme — the same PortalTheme record used to render the portal's own public page."
          }
        },
        "type": "object"
      },
      "PortalTheme": {
        "properties": {
          "backgroundStyle": {
            "pattern": "^(color|image|gradient-(aurora|dusk|meadow|ember|tide|noir))$",
            "type": "string"
          },
          "cardStyle": {
            "pattern": "^(elevated|outline|glass)$",
            "type": "string"
          },
          "colors": {
            "$ref": "#/components/schemas/PortalThemeColors"
          },
          "cornerRadius": {
            "pattern": "^(sharp|rounded|pill)$",
            "type": "string"
          },
          "fontPair": {
            "pattern": "^(geist|inter-lora|space-grotesk|playfair-source|jetbrains)$",
            "type": "string"
          },
          "hero": {
            "pattern": "^(banner|minimal|split)$",
            "type": "string"
          },
          "layout": {
            "pattern": "^(grid|masonry|list)$",
            "type": "string"
          },
          "mode": {
            "pattern": "^(dark|light)$",
            "type": "string"
          },
          "schemaVersion": {
            "format": "int32",
            "maximum": 1,
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "backgroundStyle",
          "cardStyle",
          "colors",
          "cornerRadius",
          "fontPair",
          "hero",
          "layout",
          "mode",
          "schemaVersion"
        ],
        "type": "object"
      },
      "PortalThemeColors": {
        "properties": {
          "accent": {
            "pattern": "^#[0-9a-fA-F]{6}$",
            "type": "string"
          },
          "background": {
            "pattern": "^#[0-9a-fA-F]{6}$",
            "type": "string"
          },
          "muted": {
            "pattern": "^#[0-9a-fA-F]{6}$",
            "type": "string"
          },
          "surface": {
            "pattern": "^#[0-9a-fA-F]{6}$",
            "type": "string"
          },
          "text": {
            "pattern": "^#[0-9a-fA-F]{6}$",
            "type": "string"
          }
        },
        "required": [
          "accent",
          "background",
          "muted",
          "surface",
          "text"
        ],
        "type": "object"
      },
      "PublicShareFile": {
        "description": "Per-file projection exposed via the share-page lookup.",
        "properties": {
          "contentType": {
            "description": "MIME content type of the file.",
            "type": "string"
          },
          "displayUrl": {
            "description": "Tokenised URL of the display-1080 JPEG variant, set only for HEIF-family photos (image/heic, image/heif, .heic/.heif/.hif) once the variant exists. Browsers other than Safari cannot render the original, so viewers show this instead of inlineUrl; null for every other file.",
            "type": "string"
          },
          "downloadUrl": {
            "description": "Pre-signed URL with Content-Disposition: attachment — used by download CTAs.",
            "type": "string"
          },
          "exif": {
            "$ref": "#/components/schemas/FileExifPayload",
            "description": "Whitelisted, display-formatted image metadata for the share page's EXIF chip strip. Null for non-images and for images with no readable metadata — the normal case, rendered as no strip at all. Group titles and row keys are locale-independent translation keys the frontend localises. Raw EXIF is never exposed and GPS coordinates are excluded outright; exif.gpsPresent flags only that the original carries a position (#821)."
          },
          "fileName": {
            "description": "Display name of the file as it should appear on the share page.",
            "type": "string"
          },
          "id": {
            "description": "File UUID.",
            "type": "string"
          },
          "inlineUrl": {
            "description": "Pre-signed URL with Content-Disposition: inline — used by image / video / PDF previews. Kept separate from downloadUrl so download CTAs keep the attachment behaviour unchanged.",
            "type": "string"
          },
          "paste": {
            "$ref": "#/components/schemas/SharePaste",
            "description": "Paste block (#697) — present only when this row is a text paste; null for ordinary files. The share page keys its paste view on this field, never on contentType. Absent from the locked stub."
          },
          "sizeBytes": {
            "description": "File size in bytes.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "PublicSharePathSegment": {
        "description": "One segment of the breadcrumb chain from the share root down to the currently-rendered folder.",
        "properties": {
          "id": {
            "description": "Folder id, or empty string for the root crumb (so the consumer can route to /share/:token without sentinel handling).",
            "type": "string"
          },
          "name": {
            "description": "Display name. For the root segment this is the share's folderName.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "PublicShareResponse": {
        "properties": {
          "adsEnabled": {
            "description": "True when the share is eligible to render ads (anonymous shares). False for user-owned shares.",
            "type": "boolean"
          },
          "archiveDownloadUrl": {
            "type": "string"
          },
          "canonicalSlug": {
            "description": "Short/vanity slug this share canonicalises to, WITHOUT the /s/ prefix (#571). One share is reachable as /share/<token>, /s/<auto-slug> and /s/<vanity-slug>; the SPA uses this to declare a single canonical URL and og:url so engagement does not split across the three forms. Null when the share has no slug, in which case the long token form is canonical. Set after construction, like portalBranding.",
            "type": "string"
          },
          "containingFolderShareToken": {
            "type": "string"
          },
          "expiresAt": {
            "description": "Earliest expiry across the live share members, or null when the share never expires (folder shares / paid).",
            "format": "date-time",
            "type": "string"
          },
          "files": {
            "description": "Files directly visible at the current share node. For file / multi-file shares this is the share's file set; for folder shares it is the files directly inside the requested folder.",
            "items": {
              "$ref": "#/components/schemas/PublicShareFile"
            },
            "type": "array"
          },
          "folderName": {
            "type": "string"
          },
          "indexable": {
            "description": "True when search engines may index this share page: every shared file is PUBLIC (folder share: the folder share is PUBLIC), no password, not expired, not blocked/removed, and not a paste. The page must be served noindex whenever this is false. Always false on the password-locked stub.",
            "type": "boolean"
          },
          "isOwner": {
            "type": "boolean"
          },
          "owner": {
            "type": "boolean"
          },
          "ownerFolderView": {
            "type": "boolean"
          },
          "passwordRequired": {
            "type": "boolean"
          },
          "path": {
            "items": {
              "$ref": "#/components/schemas/PublicSharePathSegment"
            },
            "type": "array"
          },
          "portalBranding": {
            "$ref": "#/components/schemas/PortalBranding"
          },
          "premium": {
            "type": "boolean"
          },
          "proofingClosedAt": {
            "format": "date-time",
            "type": "string"
          },
          "proofingEnabled": {
            "type": "boolean"
          },
          "shareMode": {
            "description": "Visibility mode of the share (e.g. PUBLIC / UNLISTED), as set on the source file_metadata / folder_share row.",
            "type": "string"
          },
          "sharedAt": {
            "description": "Latest activity timestamp — last upload for file shares, folder-share created_at for folder shares.",
            "format": "date-time",
            "type": "string"
          },
          "siblingNextToken": {
            "type": "string"
          },
          "siblingPrevToken": {
            "type": "string"
          },
          "siblings": {
            "items": {
              "$ref": "#/components/schemas/SiblingShare"
            },
            "type": "array"
          },
          "subfolders": {
            "items": {
              "$ref": "#/components/schemas/PublicShareSubfolder"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "PublicShareSubfolder": {
        "description": "Subfolder projection used by the share-page items list. Carries recursive fileCount + sizeBytes so the UI can render a '<n> files · <m> MB' hint without a follow-up round-trip.",
        "properties": {
          "fileCount": {
            "description": "Count of FILE descendants at any depth below this folder.",
            "format": "int64",
            "type": "integer"
          },
          "id": {
            "description": "Folder UUID — pass back as ?folderId=… to navigate into this subfolder.",
            "type": "string"
          },
          "name": {
            "description": "Display name of the subfolder.",
            "type": "string"
          },
          "sizeBytes": {
            "description": "Sum of sizes (bytes) of every FILE descendant at any depth.",
            "format": "int64",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "SessionStatusResponse": {
        "properties": {
          "fileId": {
            "description": "Resulting file UUID once the session reaches COMPLETE. Null while the upload is in progress.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          },
          "fileName": {
            "description": "Display name of the uploaded file as supplied by the client at session-create time.",
            "example": "vacation.jpg",
            "type": "string"
          },
          "sessionId": {
            "description": "Upload session UUID",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          },
          "status": {
            "description": "Current session state — one of PENDING, UPLOADING, COMPLETE, FAILED.",
            "enum": [
              "COMPLETE",
              "FAILED",
              "PENDING",
              "UPLOADING"
            ],
            "example": "COMPLETE",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShareExpiryRequest": {
        "description": "Body for PATCH .../share/expiry — sets or clears the expiry on an existing share link.",
        "properties": {
          "expiresInDays": {
            "description": "Days from now until the share expires. 0 or null = never expires. Server enforces the configured max-days cap via ShareExpiryResolver.",
            "example": 30,
            "format": "int32",
            "maximum": 365,
            "minimum": 0,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "ShareFileUrlsResponse": {
        "properties": {
          "displayUrl": {
            "description": "Tokenised URL of the display-1080 JPEG variant for HEIF-family photos once it exists; null otherwise. Refreshed alongside inlineUrl.",
            "type": "string"
          },
          "downloadUrl": {
            "description": "Tokenised storage-worker URL with disposition=attachment — for the explicit download CTA.",
            "type": "string"
          },
          "inlineUrl": {
            "description": "Tokenised storage-worker URL with disposition=inline — embed in <video>/<audio>/<img>.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShareLinkResponse": {
        "description": "Response envelope for share-mint (POST /api/files/{fileId}/share) and share-mode update (PATCH /api/files/{fileId}/share). Returns the recipient-facing share token, the final share mode, a relative frontend URL, an optional share-link expiry, and a password-protection flag. The backend deliberately does NOT compose absolute URLs because it cannot reliably infer the public host (dev proxies, custom domains). The password hash is never returned — only the derived boolean.",
        "properties": {
          "expiresAt": {
            "description": "Absolute share-link expiry instant (UTC). null means the share link never expires.",
            "format": "date-time",
            "type": "string"
          },
          "notifyOnView": {
            "description": "true when the share owner has opted in to email notifications on emailed-recipient first-open (issue #1228). Only fires when a tracked recipient (emailed via the share form) opens the link for the first time.",
            "type": "boolean"
          },
          "notifyOwnerOnDownload": {
            "description": "true when the share owner has opted in to email notifications on recipient download (epic #1214 Phase D).",
            "type": "boolean"
          },
          "notifyPerRecipient": {
            "description": "true when the share is in per-recipient notification mode (issue #1229): the owner receives one email per recipient download instead of only the first.",
            "type": "boolean"
          },
          "passwordProtected": {
            "description": "true when the share is protected by a password (epic #1214 Phase C). The hash is never returned.",
            "type": "boolean"
          },
          "shareMode": {
            "description": "Final share mode (SHARED | PUBLIC).",
            "type": "string"
          },
          "shareToken": {
            "description": "Recipient-facing share token.",
            "type": "string"
          },
          "shareUrl": {
            "description": "Relative frontend path ('/share/<token>'). The frontend composes the absolute URL using its own origin.",
            "type": "string"
          },
          "shortUrl": {
            "description": "Relative short-link path ('/s/<slug>') for this share (epic #1214 Phase E). null when no slug has been minted yet.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShareNotifyRequest": {
        "description": "Body for PATCH .../share/notify — controls download-notification mode for the owner of this share.",
        "properties": {
          "notifyOnDownload": {
            "description": "true to receive an email when an emailed recipient downloads; false to disable.",
            "example": true,
            "type": "boolean"
          },
          "notifyOnView": {
            "description": "Optional. true = notify the owner when an emailed recipient first opens this share link (issue #1228); false = disable view notifications; null = leave the existing setting unchanged.",
            "type": "boolean"
          },
          "notifyPerRecipient": {
            "description": "Optional. true = one notification per recipient download (per-recipient mode, issue #1229); false = one notification per share (first-download-only mode); null = leave the existing per-recipient setting unchanged.",
            "type": "boolean"
          }
        },
        "required": [
          "notifyOnDownload"
        ],
        "type": "object"
      },
      "SharePasswordRequest": {
        "description": "Body for PATCH .../share/password — sets, changes, or clears the password on an existing share link.",
        "properties": {
          "password": {
            "description": "Plaintext password (4 characters to 72 bytes of UTF-8). null = clear the password (no password protection).",
            "example": "correct-horse-42",
            "maxLength": 72,
            "minLength": 4,
            "type": "string"
          }
        },
        "type": "object"
      },
      "SharePaste": {
        "description": "Paste-specific projection on a share-page file (#697). Present only for text pastes.",
        "properties": {
          "charCount": {
            "description": "Character (code point) count of the current content.",
            "format": "int32",
            "type": "integer"
          },
          "language": {
            "description": "Display language from the closed PasteLanguage list (e.g. java, plaintext). Drives highlighting only.",
            "type": "string"
          },
          "lineCount": {
            "description": "Line count of the current content.",
            "format": "int32",
            "type": "integer"
          },
          "rawUrl": {
            "description": "Stable raw URL (/api/public/pastes/{token}/raw) serving the bytes as text/plain; null when the paste carries no share token of its own (a private paste listed inside a shared folder).",
            "type": "string"
          },
          "revision": {
            "description": "Content revision — 1 at creation, +1 per content replace.",
            "format": "int32",
            "type": "integer"
          },
          "updatedAt": {
            "description": "Instant of the last content change.",
            "format": "date-time",
            "type": "string"
          }
        },
        "type": "object"
      },
      "ShareUnlockAuditResponse": {
        "properties": {
          "bruteForceAlert": {
            "type": "boolean"
          },
          "failedLast24h": {
            "format": "int64",
            "type": "integer"
          },
          "recentEvents": {
            "items": {
              "$ref": "#/components/schemas/UnlockEventEntry"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "SiblingShare": {
        "description": "One sibling share in the current share's parent folder (issue #371). Carries the recipient-facing token + public title, a kind discriminator (file|folder), an optional image thumbnail URL, and the representative file size. Parent folder name and owner identity are never exposed.",
        "properties": {
          "kind": {
            "description": "Discriminator: 'file' for file / multi-file shares, 'folder' for folder shares. Drives the card icon.",
            "type": "string"
          },
          "sizeBytes": {
            "description": "Size in bytes of the representative image, or 0 when there is no representative image.",
            "format": "int64",
            "type": "integer"
          },
          "thumbnailUrl": {
            "description": "Signed inline URL of a representative image in the sibling share, or null when the sibling has no image (folder share, or a file share whose files are all non-image). Null → the UI renders a folder/file icon.",
            "type": "string"
          },
          "title": {
            "description": "Public display name of the sibling — same name the recipient sees on that sibling's own share page.",
            "type": "string"
          },
          "token": {
            "description": "Public, recipient-facing share token — used to build /share/{token}.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SlugAvailabilityResponse": {
        "properties": {
          "available": {
            "description": "True when the slug is free to use as a custom vanity link.",
            "type": "boolean"
          },
          "reason": {
            "description": "Reason the slug is unavailable. Absent when available=true. One of: taken | reserved | profane | invalid.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "SlugRequest": {
        "description": "Body for PATCH .../share/slug — sets or clears the custom vanity slug on an existing share link.",
        "properties": {
          "slug": {
            "description": "Custom vanity slug (3–30 chars, lowercase letters/digits/hyphens). null or blank = clear the vanity slug (revert to auto-generated short link).",
            "example": "summer-photos",
            "maxLength": 30,
            "minLength": 0,
            "type": "string"
          }
        },
        "type": "object"
      },
      "UnlockEventEntry": {
        "properties": {
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "result": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "UnlockShareRequest": {
        "properties": {
          "password": {
            "description": "The share password",
            "example": "correct-horse-42",
            "type": "string"
          },
          "recToken": {
            "description": "Optional per-recipient token from an emailed link",
            "type": "string"
          }
        },
        "type": "object"
      },
      "UnlockShareResponse": {
        "properties": {
          "share": {
            "$ref": "#/components/schemas/PublicShareResponse"
          },
          "unlockExpiresAt": {
            "format": "date-time",
            "type": "string"
          },
          "unlockToken": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateFileRequest": {
        "properties": {
          "moveToRoot": {
            "description": "Move the item to the user's Drive root. Mutually exclusive with parentId.",
            "type": "boolean"
          },
          "name": {
            "description": "New display name for the item (B.10 / #157). Max 255 chars; must not contain path separators. Server-side sanitisation additionally trims whitespace and strips ASCII control characters.",
            "example": "report-final.pdf",
            "type": "string"
          },
          "parentId": {
            "description": "Destination parent folder UUID. Mutually exclusive with moveToRoot.",
            "example": "2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateShareModeRequest": {
        "properties": {
          "shareMode": {
            "description": "New share mode (PRIVATE | SHARED | PUBLIC). Required.",
            "enum": [
              "PRIVATE",
              "PUBLIC",
              "SHARED"
            ],
            "example": "SHARED",
            "type": "string"
          }
        },
        "required": [
          "shareMode"
        ],
        "type": "object"
      },
      "UpdateShareProofingRequest": {
        "description": "Body for PATCH .../share/proofing — enables or disables client-delivery proofing on a folder share.",
        "properties": {
          "proofingEnabled": {
            "description": "true to enable client-delivery proofing (star/select + notes) on this folder share; false to disable.",
            "example": true,
            "type": "boolean"
          }
        },
        "required": [
          "proofingEnabled"
        ],
        "type": "object"
      },
      "UpdateWebhookRequest": {
        "description": "Body for PATCH /api/users/me/webhooks/{id} — partial update of a webhook. Both fields are nullable; omit to leave unchanged.",
        "properties": {
          "enabled": {
            "description": "New enabled state. True re-enables. False disables. Null leaves the state unchanged.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "url": {
            "description": "New delivery endpoint URL. Must pass SSRF validation. Null leaves the URL unchanged.",
            "example": "https://example.com/webhook",
            "maxLength": 2048,
            "minLength": 0,
            "type": [
              "null",
              "string"
            ]
          }
        },
        "type": "object"
      },
      "VisibilityBatchRequest": {
        "properties": {
          "ids": {
            "description": "Mix of file ids and folder ids to retarget. Folder ids cascade to direct file children only (depth-1). Bounded [1, 200] — caps the per-request work (one SELECT per id in FileVisibilityService).",
            "example": [
              "6f1e2d3c-4b5a-4987-8a6b-5c4d3e2f1a0b",
              "9a8b7c6d-5e4f-4321-9876-0fedcba98765"
            ],
            "items": {
              "type": "string"
            },
            "maxItems": 200,
            "minItems": 1,
            "type": "array"
          },
          "mode": {
            "description": "Target visibility. Restricted to PUBLIC (mint/keep share token + list site-wide) or PRIVATE (clear token + unlist). SHARED goes through the per-file PATCH.",
            "enum": [
              "PRIVATE",
              "PUBLIC",
              "SHARED"
            ],
            "example": "PUBLIC",
            "type": "string"
          }
        },
        "required": [
          "ids",
          "mode"
        ],
        "type": "object"
      },
      "VisibilityBatchResponse": {
        "properties": {
          "failed": {
            "description": "Per-item failures — populated even on partial success so the caller can render row-level error chips.",
            "items": {
              "$ref": "#/components/schemas/VisibilityFailure"
            },
            "type": "array"
          },
          "updated": {
            "description": "Updated FileSystemItemDto rows for files the request successfully retargeted. Includes the fresh share token / mode so the UI can refresh without a follow-up GET.",
            "items": {
              "$ref": "#/components/schemas/FileSystemItemDto"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "VisibilityFailure": {
        "properties": {
          "id": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "status": {
            "description": "Equivalent HTTP status the single-item endpoint would have returned (403, 404, 409, 410).",
            "format": "int32",
            "type": "integer"
          }
        },
        "type": "object"
      },
      "WebhookResponse": {
        "properties": {
          "consecutiveFailures": {
            "format": "int32",
            "type": "integer"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "disabledReason": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "signingSecretMasked": {
            "type": "string"
          },
          "url": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "WebhookSecretResponse": {
        "properties": {
          "createdAt": {
            "example": "2026-10-04T09:30:00Z",
            "format": "date-time",
            "type": "string"
          },
          "enabled": {
            "example": true,
            "type": "boolean"
          },
          "id": {
            "example": "1c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
            "type": "string"
          },
          "signingSecret": {
            "description": "HMAC-SHA256 signing secret, 64 lowercase hex characters. Returned only on create and rotate.",
            "example": "4f9c2b7e1a6d3c8f0b5e2a9d7c4f1e8b3a6d9c2f5e8b1a4d7c0f3e6b9a2d5c8f",
            "type": "string"
          },
          "url": {
            "example": "https://example.com/webhook",
            "type": "string"
          }
        },
        "type": "object"
      },
      "WebhookTestResponse": {
        "properties": {
          "delivered": {
            "type": "boolean"
          },
          "error": {
            "type": "string"
          },
          "statusCode": {
            "format": "int32",
            "type": "integer"
          }
        },
        "type": "object"
      }
    },
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal API key: `fmv_` + 40 characters"
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ]
}
