{
  "openapi": "3.1.0",
  "info": {
    "title": "Fileboy API",
    "version": "1.0.0",
    "summary": "File, image and video hosting.",
    "description": "Upload files, serve them from a CDN, resize images with a URL and stream video.\n\nOfficial SDKs for Ruby, Python, PHP, Node and the browser wrap this API. Guides and SDK docs: https://fileboy.io/docs.\n\n**Authentication.** Uploads send your app's public key as `Authorization: public YOUR_PUBLIC_KEY`. Apps that use a private key send a short-lived signed token as `Authorization: token PUBLIC$NONCE$JWT`. Reading files, images and video needs no authorization, unless the app requires signed reads (then every read of the app does) or the file's own read permission is `authenticated` (then reads of that file do, through its file, data, image and label URLs, and so do image URLs that draw it as an overlay): such a read needs a signed URL (`fb_exp` and `fb_sig` query parameters, see https://fileboy.io/docs/delivery/signed-urls) or the app's private key, and anything else gets 401 `signature_required` or 403. Videos follow the app setting alone. The Management endpoints (list, update and delete) only accept a token signed with the private key: a public key, or a token limited to uploads or reads, gets 403 `private_key_required`.\n\n**Errors.** Routes that report errors as JSON use `{ \"error\": \"code\", \"message\": \"...\" }`. The file, image and data read routes answer with a plain text message instead.",
    "contact": {
      "name": "Fileboy support",
      "email": "hello@fileboy.io",
      "url": "https://fileboy.io"
    },
    "license": {
      "name": "Proprietary service. SDKs are MIT licensed.",
      "url": "https://fileboy.io/terms-and-conditions"
    }
  },
  "servers": [
    {
      "url": "https://api.fileboy.io",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Files",
      "description": "Upload, read and serve files."
    },
    {
      "name": "Images",
      "description": "Serve images and transform them with a URL."
    },
    {
      "name": "Videos",
      "description": "Upload video, read processing state and stream it."
    },
    {
      "name": "Management",
      "description": "List, search, update and delete your own files, videos and labels, and change whether reads need signed URLs, from your server. Needs your private key."
    },
    {
      "name": "Folders",
      "description": "Organise your files in nested folders, from your server. Folders are never part of a file's URL, so renaming or moving one changes no links. Files with no folder are at the root. Needs your private key."
    },
    {
      "name": "Expiring links",
      "description": "Links to a file or an image transform that stop working at a time you choose and can be revoked early. Making, listing and revoking them needs your private key, opening one needs nothing."
    },
    {
      "name": "Webhooks",
      "description": "Events Fileboy sends to your server."
    },
    {
      "name": "Meta",
      "description": "Service status."
    }
  ],
  "externalDocs": {
    "description": "Fileboy documentation",
    "url": "https://fileboy.io/docs"
  },
  "paths": {
    "/": {
      "get": {
        "operationId": "getRoot",
        "tags": [
          "Meta"
        ],
        "summary": "Service banner",
        "description": "Returns the text `Fileboy API`. Useful as a reachability check.",
        "security": [],
        "responses": {
          "200": {
            "description": "Plain text.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/files": {
      "get": {
        "operationId": "listFiles",
        "tags": [
          "Management"
        ],
        "summary": "List your files",
        "description": "Your app's files, newest first by default. Needs a token signed with your private key. Images Fileboy generates when resizing, and the parts of videos, are not listed. Filters combine with AND.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page. Use it with the same filters and sort. Paging is stable: files added or deleted while you page are never skipped or repeated.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "name",
                "fileSize"
              ],
              "default": "createdAt"
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "Defaults to `desc`, except for `sort=name` which defaults to `asc`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "createdBefore",
            "in": "query",
            "description": "Only items created before this date or time (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdAfter",
            "in": "query",
            "description": "Only items created after this date or time (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "label",
            "in": "query",
            "description": "Only the file this label points at.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mime",
            "in": "query",
            "description": "Only files whose type starts with this, like `image/` or `application/pdf`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Only files whose name contains this text, ignoring case.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hash",
            "in": "query",
            "description": "Only files with this SHA-1 hash.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "metadata",
            "in": "query",
            "description": "Only files whose metadata contains these pairs. Send `metadata[key]=value` once per pair (up to 20), or one JSON object like `{\"orderId\":\"42\"}`.",
            "style": "deepObject",
            "explode": true,
            "schema": {
              "$ref": "#/components/schemas/Metadata"
            }
          },
          {
            "name": "folderId",
            "in": "query",
            "description": "Only files directly in this folder, or `root` for files in no folder. Without a folder filter every file is listed, whatever folder it is in. 404 if there is no such folder.",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "string",
                  "const": "root"
                }
              ]
            }
          },
          {
            "name": "folderPath",
            "in": "query",
            "description": "Like `folderId`, by path: `/products/shoes`, compared ignoring case. `/` is the root. Send `folderId` or `folderPath`, not both.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "recursive",
            "in": "query",
            "description": "With `folderId` or `folderPath`, include the files in its subfolders too. `folderId=root&recursive=true` is every file.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of files.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ManagedFile"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass as `cursor` for the next page. Null on the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/delete": {
      "post": {
        "operationId": "deleteFiles",
        "tags": [
          "Management"
        ],
        "summary": "Delete several files",
        "description": "Deletes up to 100 files in one request, with their stored data and cached image copies. Ids that are unknown, belong to another app or are already deleted are listed in `notFound` and are not an error, so the same request can be repeated safely. A file whose stored data cannot be removed stays in place and is listed in `failed`: send it again. Sends one `file.deleted` webhook per deleted file.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/BulkDeleteResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/upload": {
      "post": {
        "operationId": "uploadFile",
        "tags": [
          "Files"
        ],
        "summary": "Upload a file",
        "description": "Send the file as `multipart/form-data`, or send JSON with a `url` and Fileboy fetches it for you. The demo key only accepts small images and is rate limited. Uploads count towards your plan's file limit. Send `folderId` or `folderPath` to upload straight into a folder. A request signed with your private key creates any missing folders on `folderPath`; the public key and access tokens can only use folders that already exist (404 `folder_not_found` otherwise, and nothing is stored).",
        "security": [
          {
            "PublicKey": []
          },
          {
            "SignedToken": []
          },
          {
            "LegacyPublicHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  },
                  "data": {
                    "type": "string",
                    "description": "Optional JSON string of `name`, `mime`, `metadata`, `folderId` and `folderPath`. `name` and `mime` default to the uploaded file's own.",
                    "contentMediaType": "application/json",
                    "contentSchema": {
                      "$ref": "#/components/schemas/UploadFileData"
                    }
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "If present, the file is fetched from this URL and `file` is ignored."
                  },
                  "folderId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Put the file in this folder. Can also be sent inside `data`."
                  },
                  "folderPath": {
                    "type": "string",
                    "description": "Put the file in the folder at this path, like `/uploads/avatars`. Can also be sent inside `data`. See `UploadFileData`."
                  }
                }
              }
            },
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UploadFromUrl"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored file.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/File"
                    },
                    "accessToken": {
                      "type": "string",
                      "description": "Always empty."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "The folder does not exist."
          },
          "413": {
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "$ref": "#/components/responses/Error",
            "description": "`location_not_removed`: the app removes photo locations at upload (`stripLocationOnUpload`) and this photo's could not be removed, so it was not stored."
          },
          "429": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/move": {
      "post": {
        "operationId": "moveFiles",
        "tags": [
          "Folders"
        ],
        "summary": "Move files to a folder",
        "description": "Puts up to 100 files in one folder, or at the root with `folderId: null`. `folderPath` creates any missing folders on the path. Ids that are unknown or belong to another app are listed in `notFound`, so the request can be repeated safely. Moving never changes a file's URL.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ids"
                ],
                "properties": {
                  "ids": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 100,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "folderId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "The folder, or null for the root."
                  },
                  "folderPath": {
                    "type": "string",
                    "description": "The folder's path, like `/products/shoes`. `/` is the root."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each id.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/MoveFilesResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "The folder does not exist."
          }
        }
      }
    },
    "/files/get/{id}": {
      "get": {
        "operationId": "getFile",
        "tags": [
          "Files"
        ],
        "summary": "Get a file",
        "description": "Returns the stored bytes of a file. The id is a file id or `PUBLIC_KEY+LABEL` for a labelled file. If the query has a `transform` parameter the request is redirected (301) to the matching `/images/get` URL.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The file's bytes. `Content-Type` is the stored type. Supports `Range` requests.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "The requested byte range.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "301": {
            "description": "A `transform` query was given. Follow the redirect to `/images/get/{id}`."
          },
          "400": {
            "description": "The id is not a valid file id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/files/get/{id}/{filename}": {
      "get": {
        "operationId": "getFileWithName",
        "tags": [
          "Files"
        ],
        "summary": "Get a file under a chosen file name",
        "description": "Returns the stored bytes of a file. The id is a file id or `PUBLIC_KEY+LABEL` for a labelled file. If the query has a `transform` parameter the request is redirected (301) to the matching `/images/get` URL.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "description": "File name to serve the file as. If it has no extension, one is added from the file's type.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The file's bytes. `Content-Type` is the stored type. Supports `Range` requests.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "The requested byte range.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "301": {
            "description": "A `transform` query was given. Follow the redirect to `/images/get/{id}`."
          },
          "400": {
            "description": "The id is not a valid file id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/files/{id}": {
      "get": {
        "operationId": "getFileLegacy",
        "tags": [
          "Files"
        ],
        "summary": "Get a file",
        "description": "Legacy alias of `/files/get/{id}`. Returns the stored bytes of a file. The id is a file id or `PUBLIC_KEY+LABEL` for a labelled file. If the query has a `transform` parameter the request is redirected (301) to the matching `/images/get` URL.",
        "deprecated": true,
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The file's bytes. `Content-Type` is the stored type. Supports `Range` requests.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "The requested byte range.",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string"
                }
              },
              "Content-Range": {
                "description": "Present on a `206 Partial Content` response.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "301": {
            "description": "A `transform` query was given. Follow the redirect to `/images/get/{id}`."
          },
          "400": {
            "description": "The id is not a valid file id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateFile",
        "tags": [
          "Management"
        ],
        "summary": "Update a file",
        "description": "Change a file's name, type, metadata, labels, folder or share page. Only the fields you send change. `metadata` is merged into the existing metadata: keys you send are set, a `null` value removes a key and other keys stay. Send `replaceMetadata: true` to make `metadata` the complete new metadata instead. A label points at one file, so adding a label that is on another file moves it here. The id can also be `PUBLIC_KEY+LABEL`. `folderId` (null for the root) or `folderPath` moves the file to a folder; `folderPath` creates missing folders. `sharePage` turns the file's share page (https://fileboy.io/f/{id}) on or off, see `getFileShare`. The stored bytes and the file's URLs never change.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FilePatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The file as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ManagedFile"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteFile",
        "tags": [
          "Management"
        ],
        "summary": "Delete a file",
        "description": "Removes the file, its stored data and any cached image copies. Sends a `file.deleted` webhook. Answers 404 if there is no such file, which includes a file that was already deleted and a file of another app, so a repeated delete can treat 404 as done. If storage cannot be reached the answer is 500, nothing is deleted and the request can be repeated. The id can also be `PUBLIC_KEY+LABEL`.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/{id}/exif": {
      "get": {
        "operationId": "getFileExif",
        "tags": [
          "Management"
        ],
        "summary": "Get a photo's EXIF data",
        "description": "The camera, lens, exposure, date taken, orientation, GPS position, colour profile and the rest of the EXIF, IPTC and XMP metadata of an image, read from JPEG, PNG, WebP, TIFF (and DNG) and HEIF/HEIC/AVIF files. `fields` is the normalised, typed view and `raw` every tag read. It is read when the image is uploaded; an image uploaded before that is read the first time it is asked for. It includes personal data (the GPS position, place names, serial numbers and the camera owner's name), so it needs a token signed with your private key: a public key, an access token or a signed read URL is refused, and no public route ever carries it. `personal=false` leaves the personal data out. Files that are not images answer with `status: \"unsupported\"`. The id can also be `PUBLIC_KEY+LABEL`.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "personal",
            "in": "query",
            "required": false,
            "description": "`false` leaves out the GPS position, place names, serial numbers and the camera owner's name, from `fields` and `raw`.",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "default": "true"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The metadata.",
            "headers": {
              "Cache-Control": {
                "description": "`private, no-store`: it may hold personal data.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FileExif"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "502": {
            "$ref": "#/components/responses/Error",
            "description": "`exif_read_failed`: an image never read before could not be fetched from storage. Try again."
          }
        }
      }
    },
    "/files/data/{id}": {
      "get": {
        "operationId": "getFileData",
        "tags": [
          "Files"
        ],
        "summary": "Get a file's details",
        "description": "Name, type, size, hash and metadata of a file. The id is a file id or `PUBLIC_KEY+LABEL`.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The file's details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/File"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a valid file id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/files/{id}/data": {
      "get": {
        "operationId": "getFileDataLegacy",
        "tags": [
          "Files"
        ],
        "summary": "Get a file's details (legacy path)",
        "description": "Legacy alias of `/files/data/{id}`.",
        "deprecated": true,
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The file's details.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/File"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The id is not a valid file id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that id or label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/files/{id}/palette": {
      "get": {
        "operationId": "getFilePalette",
        "tags": [
          "Management"
        ],
        "summary": "Get an image's colour palette",
        "description": "The image's dominant colour, its main colours with their share of the image, the named swatches (vibrant, muted and their dark and light versions) and a readable text colour for each. It is worked out at upload; for images uploaded before then it is worked out on the first request and stored. The same palette is served on the image routes with the `palette` transform, which follows the image's read access instead of needing the private key. `{id}` can be `PUBLIC_KEY+label`.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "colors",
            "in": "query",
            "description": "How many main colours to list. The dominant colour and swatches are always included.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 16,
              "default": 6
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The palette.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Palette"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "422": {
            "description": "The file is not a JPEG, PNG, WebP, BMP or TIFF image. `error` is `unsupported_image`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/{id}/share": {
      "get": {
        "operationId": "getFileShare",
        "tags": [
          "Files"
        ],
        "summary": "Get a file's share page",
        "description": "The data behind a file's share page, https://fileboy.io/f/{id}: its name, type and size, and the URLs the page shows and downloads from. The page is off unless you turn it on (`sharePage` on `updateFile`, or the dashboard), and Fileboy's own files (image copies, the parts of a video) never have one: both are 404 `share_page_not_found`. It follows the file's read access. A public file of a public app opens for anyone. A file that needs a signature (its read permission is `authenticated`, or the app requires signed reads) needs a signed share link: sign the page URL (`https://fileboy.io/f/{id}`, scope `share`) and the page passes `fb_exp`, `fb_sig` and `fb_ip` on to this route. Without one the answer is 401 `signature_required`, whether or not the page is on. The URLs in the answer are then signed with the link's own expiry and address range. A signature for the file itself does not open the page, and the page's signature does not read the file. Only file ids are accepted, not labels. Answers are kept by the browser for at most 60 seconds, so turning a page off reaches everyone within a minute.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A file id. Labels are not accepted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The share page's data.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FileShare"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a file id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "`share_page_not_found`: no such file, its page is off, or it is one of Fileboy's own files."
          }
        }
      }
    },
    "/files/{id}/links": {
      "post": {
        "operationId": "createFileLink",
        "tags": [
          "Expiring links"
        ],
        "summary": "Make an expiring link",
        "description": "A link to the file, or to one image transform of it, that stops working at `expiresAt` and can be revoked before then. Give `expiresAt` or `expiresIn`, from a minute to 365 days away. A file can have 100 active links. The link works whatever the file's read permission or the app's signed reads setting, but it does not make a public file private: the file's own URL keeps working. The id can also be `PUBLIC_KEY+LABEL`. Files that are part of a video, and image copies Fileboy made, cannot have links.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NewFileLink"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new link. Hand out its `url`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FileLink"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "get": {
        "operationId": "listFileLinks",
        "tags": [
          "Expiring links"
        ],
        "summary": "List a file's expiring links",
        "description": "Newest first, every status unless `status=active`. Each link says how often it was opened (`useCount`, reads that started at the beginning of the file) and when it was last opened.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "status",
            "in": "query",
            "description": "`active` leaves out links that have expired or been revoked.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "all"
              ],
              "default": "all"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page. Use it with the same filters and sort. Paging is stable: files added or deleted while you page are never skipped or repeated.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of links.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FileLink"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/files/{id}/links/{linkId}": {
      "delete": {
        "operationId": "revokeFileLink",
        "tags": [
          "Expiring links"
        ],
        "summary": "Revoke an expiring link",
        "description": "The link stops working now: reads answer 410 `link_revoked`. Safe to repeat, a link that is already revoked keeps its first revocation time. A browser that already downloaded the file keeps its copy, for at most five minutes of cache.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "linkId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The link, now revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FileLink"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/links/{token}": {
      "get": {
        "operationId": "getLink",
        "tags": [
          "Expiring links"
        ],
        "summary": "Open an expiring link",
        "description": "Serves what the link was made for, exactly as `/files/get/{id}` or `/images/get/{id}?transform=...` would, while the link is active. Needs nothing else: the link is the permission. The query string is ignored, so a link cannot be changed into another transform or a download. `Range` works (seeking in audio and video) and `format:auto` follows `Accept`. Responses are `Cache-Control: private` for at most five minutes and never past the expiry, with `X-Robots-Tag: noindex`.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "The last part of the link's `url`.",
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{24}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file or the transformed image.",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "206": {
            "description": "Part of the file, for a `Range` request.",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "`link_not_found`: there is no such link, or its file was deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`link_expired` or `link_revoked`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "head": {
        "operationId": "headLink",
        "tags": [
          "Expiring links"
        ],
        "summary": "Check an expiring link",
        "description": "Like GET, without the body. Not counted as a use.",
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Za-z0-9_-]{24}$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The link is active."
          },
          "404": {
            "description": "`link_not_found`."
          },
          "410": {
            "description": "`link_expired` or `link_revoked`."
          }
        }
      }
    },
    "/images/get/{id}": {
      "get": {
        "operationId": "getImage",
        "tags": [
          "Images"
        ],
        "summary": "Get an image, optionally transformed",
        "description": "Serves an image, running the transforms in the `transform` query parameter in order. Results are cached per transform chain. A transform that does not parse, or would make the image too large, is answered with 400 and a JSON body saying what is wrong. Files that are not JPEG, PNG, WebP, BMP or TIFF (GIF, SVG, PDF...) are served unchanged. JPEG orientation from EXIF is applied, and transformed output carries no metadata. With `format:auto` the response varies on `Accept` and says so in `Vary`. Animated GIF and WebP are served unchanged. See the `transform` parameter for the grammar.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/Transform"
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The image. `Content-Type` is the output format. With `format:json`, a JSON description of the image instead; with `palette`, its colours as JSON (a `Palette`) or, with `palette:css`, a stylesheet.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/css": {
                "schema": {
                  "type": "string",
                  "description": "`palette:css`: custom properties on `:root` for every colour and its text colour, and `-bg-` and `-fg-` classes."
                }
              },
              "application/json": {
                "schema": {
                  "description": "`ImageInfo` when the chain ends with `format:json`, `Palette` when it ends with `palette` (or `palette:json`).",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ImageInfo"
                    },
                    {
                      "$ref": "#/components/schemas/Palette"
                    }
                  ]
                },
                "examples": {
                  "resized": {
                    "summary": "format:json",
                    "value": {
                      "width": 300,
                      "height": 200,
                      "original": {
                        "width": 1200,
                        "height": 800,
                        "format": "image/jpeg",
                        "bytes": 254311
                      }
                    }
                  },
                  "palette": {
                    "summary": "palette~colors:1",
                    "value": {
                      "version": 1,
                      "dominant": {
                        "hex": "#3d5a73",
                        "rgb": [
                          61,
                          90,
                          115
                        ],
                        "share": 0.4213,
                        "text": "#ffffff",
                        "contrast": 7.22
                      },
                      "colors": [
                        {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        }
                      ],
                      "swatches": {
                        "vibrant": null,
                        "darkVibrant": null,
                        "lightVibrant": null,
                        "muted": {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        },
                        "darkMuted": null,
                        "lightMuted": null
                      },
                      "transparentShare": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The transform is not valid, the image is too large to transform, or an overlay is not an image file of this app (JSON, `error` is `invalid_transform`), or the id is not valid or the file could not be read (plain text).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "A `palette` was asked for a file that is not a JPEG, PNG, WebP, BMP or TIFF image. `error` is `unsupported_image`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The image could not be transformed. `error` is `transform_failed` or `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/get/{id}/{filename}": {
      "get": {
        "operationId": "getImageWithName",
        "tags": [
          "Images"
        ],
        "summary": "Get an image under a chosen file name",
        "description": "Serves an image, running the transforms in the `transform` query parameter in order. Results are cached per transform chain. A transform that does not parse, or would make the image too large, is answered with 400 and a JSON body saying what is wrong. Files that are not JPEG, PNG, WebP, BMP or TIFF (GIF, SVG, PDF...) are served unchanged. JPEG orientation from EXIF is applied, and transformed output carries no metadata. With `format:auto` the response varies on `Accept` and says so in `Vary`. Animated GIF and WebP are served unchanged. See the `transform` parameter for the grammar.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "name": "filename",
            "in": "path",
            "required": true,
            "description": "File name to serve the file as. If it has no extension, one is added from the file's type.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Transform"
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The image. `Content-Type` is the output format. With `format:json`, a JSON description of the image instead; with `palette`, its colours as JSON (a `Palette`) or, with `palette:css`, a stylesheet.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/css": {
                "schema": {
                  "type": "string",
                  "description": "`palette:css`: custom properties on `:root` for every colour and its text colour, and `-bg-` and `-fg-` classes."
                }
              },
              "application/json": {
                "schema": {
                  "description": "`ImageInfo` when the chain ends with `format:json`, `Palette` when it ends with `palette` (or `palette:json`).",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ImageInfo"
                    },
                    {
                      "$ref": "#/components/schemas/Palette"
                    }
                  ]
                },
                "examples": {
                  "resized": {
                    "summary": "format:json",
                    "value": {
                      "width": 300,
                      "height": 200,
                      "original": {
                        "width": 1200,
                        "height": 800,
                        "format": "image/jpeg",
                        "bytes": 254311
                      }
                    }
                  },
                  "palette": {
                    "summary": "palette~colors:1",
                    "value": {
                      "version": 1,
                      "dominant": {
                        "hex": "#3d5a73",
                        "rgb": [
                          61,
                          90,
                          115
                        ],
                        "share": 0.4213,
                        "text": "#ffffff",
                        "contrast": 7.22
                      },
                      "colors": [
                        {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        }
                      ],
                      "swatches": {
                        "vibrant": null,
                        "darkVibrant": null,
                        "lightVibrant": null,
                        "muted": {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        },
                        "darkMuted": null,
                        "lightMuted": null
                      },
                      "transparentShare": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The transform is not valid, the image is too large to transform, or an overlay is not an image file of this app (JSON, `error` is `invalid_transform`), or the id is not valid or the file could not be read (plain text).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "A `palette` was asked for a file that is not a JPEG, PNG, WebP, BMP or TIFF image. `error` is `unsupported_image`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The image could not be transformed. `error` is `transform_failed` or `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/images/{id}": {
      "get": {
        "operationId": "getImageLegacy",
        "tags": [
          "Images"
        ],
        "summary": "Get an image, optionally transformed",
        "description": "Legacy alias of `/images/get/{id}`. Serves an image, running the transforms in the `transform` query parameter in order. Results are cached per transform chain. A transform that does not parse, or would make the image too large, is answered with 400 and a JSON body saying what is wrong. Files that are not JPEG, PNG, WebP, BMP or TIFF (GIF, SVG, PDF...) are served unchanged. JPEG orientation from EXIF is applied, and transformed output carries no metadata. With `format:auto` the response varies on `Accept` and says so in `Vary`. Animated GIF and WebP are served unchanged. See the `transform` parameter for the grammar.",
        "deprecated": true,
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/FileId"
          },
          {
            "$ref": "#/components/parameters/Transform"
          },
          {
            "$ref": "#/components/parameters/Download"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The image. `Content-Type` is the output format. With `format:json`, a JSON description of the image instead; with `palette`, its colours as JSON (a `Palette`) or, with `palette:css`, a stylesheet.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "text/css": {
                "schema": {
                  "type": "string",
                  "description": "`palette:css`: custom properties on `:root` for every colour and its text colour, and `-bg-` and `-fg-` classes."
                }
              },
              "application/json": {
                "schema": {
                  "description": "`ImageInfo` when the chain ends with `format:json`, `Palette` when it ends with `palette` (or `palette:json`).",
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ImageInfo"
                    },
                    {
                      "$ref": "#/components/schemas/Palette"
                    }
                  ]
                },
                "examples": {
                  "resized": {
                    "summary": "format:json",
                    "value": {
                      "width": 300,
                      "height": 200,
                      "original": {
                        "width": 1200,
                        "height": 800,
                        "format": "image/jpeg",
                        "bytes": 254311
                      }
                    }
                  },
                  "palette": {
                    "summary": "palette~colors:1",
                    "value": {
                      "version": 1,
                      "dominant": {
                        "hex": "#3d5a73",
                        "rgb": [
                          61,
                          90,
                          115
                        ],
                        "share": 0.4213,
                        "text": "#ffffff",
                        "contrast": 7.22
                      },
                      "colors": [
                        {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        }
                      ],
                      "swatches": {
                        "vibrant": null,
                        "darkVibrant": null,
                        "lightVibrant": null,
                        "muted": {
                          "hex": "#3d5a73",
                          "rgb": [
                            61,
                            90,
                            115
                          ],
                          "share": 0.4213,
                          "text": "#ffffff",
                          "contrast": 7.22
                        },
                        "darkMuted": null,
                        "lightMuted": null
                      },
                      "transparentShare": 0
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The transform is not valid, the image is too large to transform, or an overlay is not an image file of this app (JSON, `error` is `invalid_transform`), or the id is not valid or the file could not be read (plain text).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "description": "No file with that label.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "A `palette` was asked for a file that is not a JPEG, PNG, WebP, BMP or TIFF image. `error` is `unsupported_image`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "The image could not be transformed. `error` is `transform_failed` or `internal_error`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/labels": {
      "get": {
        "operationId": "listLabels",
        "tags": [
          "Management"
        ],
        "summary": "List labels",
        "description": "Your app's labels, by name. A label is a name that points at one file, so you can use `PUBLIC_KEY+LABEL` in place of a file id.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "All labels.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Label"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "post": {
        "operationId": "createLabel",
        "tags": [
          "Management"
        ],
        "summary": "Create a label",
        "description": "Label names are 1 to 100 characters with no spaces and none of `- + / \\ ? # % &`. Optionally point it at a file straight away. Answers 409 if the name is taken.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "fileId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new label.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Label"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "`fileId` is not one of your files."
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/labels/{name}": {
      "delete": {
        "operationId": "deleteLabel",
        "tags": [
          "Management"
        ],
        "summary": "Delete a label",
        "description": "Removes the label, not the file it points at. 404 if there is no such label.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/folders": {
      "get": {
        "operationId": "listFolders",
        "tags": [
          "Folders"
        ],
        "summary": "List folders",
        "description": "Your app's folders, by name. With `parentId` only the folders directly inside one folder (`root` for the top level), otherwise every folder. `path` looks up one folder by its path and returns it, or an empty list.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page. Use it with the same filters and sort. Paging is stable: files added or deleted while you page are never skipped or repeated.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "createdAt"
              ],
              "default": "name"
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "Defaults to `desc`, except for `sort=name` which defaults to `asc`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "parentId",
            "in": "query",
            "description": "Only the folders directly inside this folder, or `root` for the top level.",
            "schema": {
              "oneOf": [
                {
                  "type": "string",
                  "format": "uuid"
                },
                {
                  "type": "string",
                  "const": "root"
                }
              ]
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Only folders whose name contains this text, ignoring case.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "query",
            "description": "Find the folder at this path, like `/products/shoes`, ignoring case.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of folders.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Folder"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Pass as `cursor` for the next page. Null on the last page."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "`parentId` is not one of your folders."
          }
        }
      },
      "post": {
        "operationId": "createFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Create a folder",
        "description": "Send a `name`, and a `parentId` to make it inside another folder: 201, or 409 `folder_exists` if that folder already has one of the same name, ignoring case. Or send a `path` to make every missing folder on it, like `mkdir -p`: 201 if anything was made, 200 if it all existed. Names are 1 to 255 characters with no `/` or `\\`, and cannot be `.` or `..`. Folders nest up to 20 deep and an app can have up to 10,000.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "parentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid"
                  },
                  "path": {
                    "type": "string",
                    "examples": [
                      "/products/shoes"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Every folder on `path` already existed. The last one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "The new folder (with `path`, the last folder on it).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "No private key, or `folder_limit_reached`."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "`parentId` is not one of your folders."
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/folders/{id}": {
      "get": {
        "operationId": "getFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Get a folder",
        "description": "One folder, with the folders above it in `ancestors` for breadcrumbs.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A folder id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The folder.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "updateFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Rename or move a folder",
        "description": "Send `name` to rename it and `parentId` to move it, with what is inside, into another folder (`null` for the root). A folder cannot move into itself or one of its subfolders (400 `invalid_move`), and the destination cannot already have a folder of the same name (409 `folder_exists`). No file URLs change.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A folder id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255
                  },
                  "parentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The folder as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Folder"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteFolder",
        "tags": [
          "Folders"
        ],
        "summary": "Delete a folder",
        "description": "`contents` says what happens to what is inside. `refuse` (the default) only deletes an empty folder and answers 409 `folder_not_empty` otherwise. `moveToParent` moves its files and subfolders up into its parent (the root for a top-level folder), and answers 409 `folder_exists` if a subfolder's name is taken there. `delete` deletes its subfolders and every file in them, stored data included, with a `file.deleted` webhook for each: up to 1000 files, more is 409 `folder_too_large`. If storage cannot be reached, the files already deleted stay deleted, the folder is kept and the request can be repeated.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "A folder id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "contents",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "refuse",
                "moveToParent",
                "delete"
              ],
              "default": "refuse"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "What was deleted and moved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/FolderDeleteResult"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "Management"
        ],
        "summary": "Get your app's usage",
        "description": "How many files and videos the app has and how many bytes it stores (originals, image copies, video renditions, posters and transcripts), with the limits set on the app. Transfer is not reported.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current usage.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Usage"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/usage/alerts": {
      "get": {
        "operationId": "getUsageAlerts",
        "tags": [
          "Management"
        ],
        "summary": "Get your usage alert settings and this month's alerts",
        "description": "When the app reaches a share of a plan limit (80% and 100% unless you change it) Fileboy emails the app's members and any extra recipients, shows a banner in the dashboard and sends a `usage.threshold_reached` webhook. Each threshold fires at most once a month (calendar months in UTC) per limit, and every threshold can fire again from the 1st. Usage is checked every few minutes. Files, videos and storage can alert; transfer is not measured per app yet.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The settings, the current period and the alerts recorded in it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UsageAlerts"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "updateUsageAlerts",
        "tags": [
          "Management"
        ],
        "summary": "Change your usage alert settings",
        "description": "Only the fields you send change, and `metrics` only changes the limits it names. Thresholds are sorted and duplicates removed; email addresses are lower cased. Answers 400 `validation_error` for a value that is not allowed, `invalid_json` for an unknown field and `empty_update` when nothing is sent.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "thresholds": {
                    "$ref": "#/components/schemas/UsageAlertThresholds"
                  },
                  "metrics": {
                    "type": "object",
                    "additionalProperties": false,
                    "properties": {
                      "files": {
                        "type": "boolean"
                      },
                      "videos": {
                        "type": "boolean"
                      },
                      "storage": {
                        "type": "boolean"
                      }
                    }
                  },
                  "emailMembers": {
                    "type": "boolean"
                  },
                  "recipients": {
                    "$ref": "#/components/schemas/UsageAlertRecipients"
                  },
                  "webhook": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new settings, the current period and its alerts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UsageAlerts"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/usage/storage": {
      "get": {
        "operationId": "getStorageUsage",
        "tags": [
          "Management"
        ],
        "summary": "Get your app's storage breakdown",
        "description": "What the app stores by kind, its biggest files and videos, and one total a day. Every stored file counts once, under one kind, so the kinds add up to `storageBytes`, the same total `GET /usage` reports and the app's storage limit is measured against. Daily totals are recorded just after midnight UTC and kept for 400 days; days before tracking started are missing rather than filled in.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "description": "How many days of daily totals to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 30
            }
          },
          {
            "name": "top",
            "in": "query",
            "description": "How many of the biggest files and videos to list.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The storage breakdown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/StorageUsage"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/app/settings": {
      "get": {
        "operationId": "getAppSettings",
        "tags": [
          "Management"
        ],
        "summary": "Get the app's read access, photo location and player branding settings",
        "description": "Whether reads of files, images and videos need a signed URL (off by default: nothing is private until you turn it on), whether GPS positions are removed from photos at upload (off by default), and the player branding every video starts with.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "The settings.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AppSettings"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "patch": {
        "operationId": "updateAppSettings",
        "tags": [
          "Management"
        ],
        "summary": "Require signed URLs to read, remove photo locations at upload, or set the player branding",
        "description": "Send any of the settings. With `requireSignedReads: true`, every unsigned link to the app's files, images and videos stops working at once (401 `signature_required`) until it is signed with the private key. Applies to every route that reads a file, image or video, including video manifests, segments, posters and captions. Turning it off makes every link public again. With `stripLocationOnUpload: true`, the GPS position is removed from the EXIF and XMP data of every image uploaded from then on, before it is stored. The file is not re-encoded and nothing else in it changes; its `hash` is of the stored bytes. Files already uploaded are not changed.\n\n`playerBranding` replaces the app's player branding (null clears it). It applies to every video that does not set the same field itself, on the watch and embed pages and in `<fileboy-player branding>`. Nothing changes unless everything sent is valid.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": false,
                "properties": {
                  "requireSignedReads": {
                    "type": "boolean"
                  },
                  "stripLocationOnUpload": {
                    "type": "boolean"
                  },
                  "playerBranding": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/PlayerBranding"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The settings as they are now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/AppSettings"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos": {
      "get": {
        "operationId": "listVideos",
        "tags": [
          "Management"
        ],
        "summary": "List your videos",
        "description": "Your app's videos, newest first by default. Every video has its `description`, `tags` and `metadata`. Needs a token signed with your private key.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1 to 100.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "The `nextCursor` of the previous page. Use it with the same filters and sort. Paging is stable: files added or deleted while you page are never skipped or repeated.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "createdAt",
                "name"
              ],
              "default": "createdAt"
            }
          },
          {
            "name": "order",
            "in": "query",
            "description": "Defaults to `desc`, except for `sort=name` which defaults to `asc`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          },
          {
            "name": "createdBefore",
            "in": "query",
            "description": "Only items created before this date or time (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "createdAfter",
            "in": "query",
            "description": "Only items created after this date or time (ISO 8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "name",
            "in": "query",
            "description": "Only videos whose name contains this text, ignoring case.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pending",
                "processing",
                "ready",
                "failed"
              ]
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Only videos that have every one of these tags (up to 20). Comma-separated, `tags=launch,2026`, or repeated, `tags=launch&tags=2026`. Tags are case-sensitive.",
            "style": "form",
            "explode": false,
            "schema": {
              "type": "array",
              "maxItems": 20,
              "items": {
                "type": "string",
                "maxLength": 64
              }
            }
          },
          {
            "name": "metadata",
            "in": "query",
            "description": "Only videos whose metadata contains these pairs. Send `metadata[key]=value` once per pair (up to 20), or one JSON object like `{\"orderId\":\"42\"}`.",
            "style": "deepObject",
            "explode": true,
            "schema": {
              "$ref": "#/components/schemas/Metadata"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "One page of videos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data",
                    "nextCursor"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ManagedVideo"
                      }
                    },
                    "nextCursor": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}": {
      "patch": {
        "operationId": "updateVideo",
        "tags": [
          "Management"
        ],
        "summary": "Change a video's name, description, tags, metadata or player branding",
        "description": "Only the fields you send change, and a request that would break a limit, or has an invalid field, changes nothing. `description` is public: the watch page and `/videos/{id}/data` show it. `tags` and `metadata` are private: they are only in management responses and webhooks. `tags` is a list that replaces the tags, `null` to remove them all, or `{\"add\": [...], \"remove\": [...]}` to change some. `metadata` is merged like a file's: a `null` value removes a key, `replaceMetadata: true` replaces it all. `playerBranding` is this video's own branding on top of the app's: each field it sets replaces the app's, `controls` merge control by control, and null makes the video follow the app again.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VideoPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The video as it is now.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ManagedVideo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "delete": {
        "operationId": "deleteVideo",
        "tags": [
          "Management"
        ],
        "summary": "Delete a video",
        "description": "Removes the original, the posters, the transcripts and every rendition from storage, then the video. Sends a `video.deleted` webhook. 404 if there is no such video or it was already deleted. If storage cannot be reached the answer is 500: the video is still listed, with whatever was not yet removed, and the request can be repeated to finish.",
        "security": [
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/upload": {
      "post": {
        "operationId": "uploadVideo",
        "tags": [
          "Videos"
        ],
        "summary": "Upload a video",
        "description": "The file's content type must be `video/*`. Encoding happens in the background: poll `/videos/{id}/data` until `extractionStatus` finishes. Not available with the demo key.",
        "security": [
          {
            "PublicKey": []
          },
          {
            "SignedToken": []
          },
          {
            "LegacyPublicHeader": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "data"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The video."
                  },
                  "data": {
                    "type": "string",
                    "description": "A JSON string.",
                    "contentMediaType": "application/json",
                    "contentSchema": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string",
                          "maxLength": 5000,
                          "description": "Plain text shown on the watch page."
                        },
                        "tags": {
                          "$ref": "#/components/schemas/VideoTags"
                        },
                        "metadata": {
                          "$ref": "#/components/schemas/Metadata"
                        }
                      }
                    }
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "*/*"
                },
                "data": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new video. This response is not wrapped in `data`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/data": {
      "get": {
        "operationId": "getVideoData",
        "tags": [
          "Videos"
        ],
        "summary": "Get a video's processing state and renditions",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The video.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Video"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a valid video id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "No such video exists."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/info": {
      "get": {
        "operationId": "getVideoInfo",
        "tags": [
          "Videos"
        ],
        "summary": "Get a video's basic details",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The video. Not wrapped in `data`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoInfo"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not valid or no such video exists."
          }
        }
      }
    },
    "/videos/{id}/player-config": {
      "get": {
        "operationId": "getVideoPlayerConfig",
        "tags": [
          "Videos"
        ],
        "summary": "Get the player branding for a video",
        "description": "The app's player branding with the video's own on top, ready for a player: colours, the logo as an image URL, every control's switch, a download URL for the original when downloads are on, and the autoplay, muted and loop defaults. `<fileboy-player branding>` and the watch and embed pages read it. A video with no branding gets the defaults (no colours or logo, every control but download). In an app that requires signed reads it needs the video's signature like the other video routes, and the logo and download URLs come back signed to expire with it. Cached for at most a minute, so dashboard changes show quickly.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The resolved branding.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PlayerConfig"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a valid video id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "No such video exists."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/manifest": {
      "get": {
        "operationId": "getVideoManifest",
        "tags": [
          "Videos"
        ],
        "summary": "Get the streaming manifest",
        "description": "An HLS (`application/x-mpegURL`) or DASH (`application/dash+xml`) manifest for players. Each GET counts as a play. `type=auto` picks DASH for Chrome and HLS for everything else.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/ManifestType"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The manifest. `Range` requests get `206`.",
            "content": {
              "application/x-mpegURL": {
                "schema": {
                  "type": "string"
                }
              },
              "application/dash+xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid type or id, no such video, or the video is still processing."
          }
        }
      },
      "head": {
        "operationId": "headVideoManifest",
        "tags": [
          "Videos"
        ],
        "summary": "Check the streaming manifest",
        "description": "Same as the GET, without the body and without counting a play.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/ManifestType"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers only."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/hls-sub-manifest": {
      "get": {
        "operationId": "getVideoHlsSubManifest",
        "tags": [
          "Videos"
        ],
        "summary": "Get one rendition's HLS playlist",
        "description": "Referenced by the main HLS manifest. Players follow it automatically.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "name": "videoFileId",
            "in": "query",
            "required": true,
            "description": "A rendition's id from `videoFiles` in the video data.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "An HLS media playlist.",
            "content": {
              "application/x-mpegURL": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      },
      "head": {
        "operationId": "headVideoHlsSubManifest",
        "tags": [
          "Videos"
        ],
        "summary": "Check one rendition's HLS playlist",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "name": "videoFileId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers only."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/file": {
      "get": {
        "operationId": "getVideoFile",
        "tags": [
          "Videos"
        ],
        "summary": "Get one encoded rendition",
        "description": "Referenced by the manifests. Supports `Range` requests.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "name": "file",
            "in": "query",
            "required": true,
            "description": "The rendition's file name, as used in the manifest.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The bytes.",
            "content": {
              "*/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/poster": {
      "get": {
        "operationId": "getVideoPoster",
        "tags": [
          "Videos"
        ],
        "summary": "Get a video's poster image",
        "description": "The custom poster if one was uploaded, otherwise the generated frame.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/*": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error",
            "description": "No such video, or it has no poster yet."
          }
        }
      }
    },
    "/videos/{id}/poster/upload": {
      "post": {
        "operationId": "uploadVideoPoster",
        "tags": [
          "Videos"
        ],
        "summary": "Upload a custom poster",
        "description": "The content type must be `image/*`. Not available with the demo key.",
        "security": [
          {
            "PublicKey": []
          },
          {
            "SignedToken": []
          },
          {
            "LegacyPublicHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored image file.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/transcripts/upload": {
      "post": {
        "operationId": "uploadVideoTranscript",
        "tags": [
          "Videos"
        ],
        "summary": "Attach a transcript",
        "description": "The content type must be `text/*` (WebVTT or plain text). Not available with the demo key.",
        "security": [
          {
            "PublicKey": []
          },
          {
            "SignedToken": []
          },
          {
            "LegacyPublicHeader": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file",
                  "data"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The transcript."
                  },
                  "data": {
                    "type": "string",
                    "description": "A JSON string.",
                    "contentMediaType": "application/json",
                    "contentSchema": {
                      "type": "object",
                      "required": [
                        "language"
                      ],
                      "properties": {
                        "language": {
                          "type": "string",
                          "examples": [
                            "English"
                          ]
                        }
                      }
                    }
                  }
                }
              },
              "encoding": {
                "file": {
                  "contentType": "*/*"
                },
                "data": {
                  "contentType": "application/json"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id"
                      ],
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/thumbnails.vtt": {
      "get": {
        "operationId": "getVideoThumbnailsVtt",
        "tags": [
          "Videos"
        ],
        "summary": "Get a video's seek-preview thumbnails as WebVTT",
        "description": "One cue per frame, each pointing at its region of a sprite sheet with a media fragment (`.../thumbnails/0.jpg?v=...#xywh=x,y,w,h`). The format the Fileboy player, Plyr and most other players read for previews over the progress bar. Sheet URLs are absolute and carry this request's signature on, so in an app that requires signed reads sign this URL (or add `signVideo`'s query) and the player can follow the rest. Up to 100 frames: one a second for videos up to 100 seconds, spread evenly over longer ones.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The thumbnails track.",
            "content": {
              "text/vtt": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a valid video id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "No such video (`video_not_found`), or its thumbnails are not made yet, being made, or failed (`thumbnails_not_found`). See `thumbnails.status` in the video's data."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/thumbnails.json": {
      "get": {
        "operationId": "getVideoThumbnailsJson",
        "tags": [
          "Videos"
        ],
        "summary": "Get a video's seek-preview thumbnails as a JSON storyboard",
        "description": "The same thumbnails as the WebVTT, for players that would rather not parse it: the tile size, the sheets and every tile's time range and position. URLs carry this request's signature on.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The storyboard.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ThumbnailStoryboard"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a valid video id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "No such video, or its thumbnails are not ready (`thumbnails_not_found`)."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/thumbnails/{sheet}.jpg": {
      "get": {
        "operationId": "getVideoThumbnailSheet",
        "tags": [
          "Videos"
        ],
        "summary": "Get a sprite sheet of a video's seek-preview thumbnails",
        "description": "A JPEG grid of frames, 10 to a row. The VTT and JSON storyboard link to these with a `v` parameter that changes when the thumbnails are remade, so they can be cached for long.",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "name": "sheet",
            "in": "path",
            "required": true,
            "description": "The sheet's number, from 0.",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "v",
            "in": "query",
            "required": false,
            "description": "Cache key the VTT and JSON add. Ignored by the server.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The sheet.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "The id is not a valid video id."
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "No such video or sheet, or the thumbnails are not ready."
          },
          "500": {
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/videos/{id}/transcripts/{transcriptId}": {
      "get": {
        "operationId": "getVideoTranscript",
        "tags": [
          "Videos"
        ],
        "summary": "Get a transcript",
        "security": [
          {},
          {
            "ReadSignature": []
          },
          {
            "SignedToken": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/VideoId"
          },
          {
            "name": "transcriptId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/SignatureExpires"
          },
          {
            "$ref": "#/components/parameters/Signature"
          },
          {
            "$ref": "#/components/parameters/SignatureIp"
          }
        ],
        "responses": {
          "200": {
            "description": "The transcript text.",
            "content": {
              "text/*": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SignatureRequired"
          },
          "403": {
            "$ref": "#/components/responses/SignatureInvalid"
          },
          "500": {
            "$ref": "#/components/responses/Error",
            "description": "No such video or transcript."
          }
        }
      }
    }
  },
  "webhooks": {
    "fileboyEvent": {
      "post": {
        "operationId": "receiveWebhook",
        "tags": [
          "Webhooks"
        ],
        "summary": "An event Fileboy sends to your endpoint",
        "description": "Fileboy calls the URL of each webhook endpoint you add in the dashboard. Reply with any 2xx status within a few seconds. Failed deliveries are retried for several hours. Check `Fileboy-Signature` (`v1=` and the hex HMAC-SHA256, keyed with the endpoint's signing secret, of `<Fileboy-Timestamp>.<raw body>`) and reject timestamps more than five minutes from now. The Node, Ruby, Python and PHP libraries do this for you. Events carry a unique `id` that is the same on every retry. When an endpoint keeps failing (by default 5 attempts in a row over at least 15 minutes) the app's members are emailed once, and again when it recovers. Apps can opt in to having an endpoint that keeps failing for days disabled automatically, after a warning email.",
        "parameters": [
          {
            "name": "Fileboy-Timestamp",
            "in": "header",
            "required": true,
            "description": "Unix time in seconds when the delivery was sent.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "Fileboy-Signature",
            "in": "header",
            "required": true,
            "description": "`v1=` followed by the hex signature.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Received. Any 2xx status counts as delivered."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "PublicKey": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "`public YOUR_PUBLIC_KEY`. Safe to use in browser code. Limited by the app's permission settings."
      },
      "SignedToken": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "`token PUBLIC_KEY$NONCE$JWT`. Built on your server with the private key: the JWT is HS256 signed with the private key and carries `{ v: 3, token: NONCE, iat, exp }`. The SDKs create these for you. The token may also be sent as an `authorization` query parameter, except for reads that need a signature (an app that requires signed reads, or a file whose read permission is `authenticated`), where only the header is accepted."
      },
      "ReadSignature": {
        "type": "apiKey",
        "in": "query",
        "name": "fb_sig",
        "description": "A signed URL, for apps that require signed reads and for files whose read permission is `authenticated`. Add `fb_exp` (unix seconds), `fb_sig` and optionally `fb_ip` to the URL. `fb_sig` is the base64url (no padding) HMAC-SHA256, keyed with the app's private key, of a canonical string that names the file or video, the transform and filename, the expiry and the address. The SDKs make these for you (`signUrl`), and the format is in https://fileboy.io/docs/delivery/signed-urls."
      },
      "LegacyPublicHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Fileboy-Public",
        "description": "Older way to send the public key. Equivalent to `Authorization: public KEY`."
      }
    },
    "parameters": {
      "FileId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "A file id (UUID), or `PUBLIC_KEY+LABEL` to address a labelled file.",
        "schema": {
          "type": "string"
        },
        "examples": {
          "id": {
            "value": "2a860257-ecf9-424a-846e-00ab68e2fd58"
          },
          "label": {
            "value": "YOUR_PUBLIC_KEY+logo"
          }
        }
      },
      "VideoId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The video's id.",
        "schema": {
          "type": "string",
          "format": "uuid"
        }
      },
      "Download": {
        "name": "download",
        "in": "query",
        "required": false,
        "description": "Add `?download` to send the file as an attachment so browsers save it. `download=false` turns it off.",
        "allowEmptyValue": true,
        "schema": {
          "type": "string"
        }
      },
      "SignatureExpires": {
        "name": "fb_exp",
        "in": "query",
        "required": false,
        "description": "Unix time in seconds after which a signed URL stops working (30 seconds of leeway for clock differences). Only used for reads that need a signature (apps that require signed reads, files whose read permission is `authenticated`).",
        "schema": {
          "type": "integer",
          "format": "int64"
        }
      },
      "Signature": {
        "name": "fb_sig",
        "in": "query",
        "required": false,
        "description": "The signature of a signed URL. Only used for reads that need a signature (apps that require signed reads, files whose read permission is `authenticated`), ignored otherwise.",
        "schema": {
          "type": "string",
          "pattern": "^[A-Za-z0-9_-]{43}$"
        }
      },
      "SignatureIp": {
        "name": "fb_ip",
        "in": "query",
        "required": false,
        "description": "An IP address or CIDR range the request must come from. Part of the signature, so it cannot be added or removed afterwards.",
        "schema": {
          "type": "string"
        }
      },
      "ManifestType": {
        "name": "type",
        "in": "query",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "auto",
            "hls",
            "dash"
          ],
          "default": "auto"
        }
      },
      "Transform": {
        "name": "transform",
        "in": "query",
        "required": false,
        "description": "Transforms joined by `;` and run in order, each written `name:value~option:value`. At most 20 transforms and 2048 characters.\n\n**Size and shape**: `resize:WxH` (`_` leaves a side free; options `~fit:cover|contain|fill|inside|outside`, `~position:top-left|top|top-right|left|center|right|bottom-left|bottom|bottom-right`, `~background:COLOUR`, `~format:FORMAT`, `~focus:X,Y` to keep a point in view with fit cover, `~upscale:false` to never make the image bigger than its source), `resizeAR:WxH` (crop or pad to an aspect ratio; same options as resize except focus), `crop:WxH` (sizes in px or `50pc`; `@X,Y` places the centre of the crop, or `~position:` anchors it), `smartCrop:WxH` (crops to the most detailed part of the image), `rotate:DEGREES` (clockwise, `~background:COLOUR`), `flip:x|y|xy`, `flop`, `radius:PX|max` (rounded corners or a circle, `~background:`), `pad:ALL|V,H|T,R,B,L` (`~background:`), `border:PX` (`~color:`), `trim` or `trim:0-100` (cuts away a border of one colour, the top-left pixel's unless `~color:` is given; the number is the tolerance, default 10), `dpr:N` (multiplies every pixel size, 0.1 to 5).\n\n**Look**: `blur:0-100`, `sharpen` or `sharpen:0-10`, `grayscale`, `saturate:N` (0 grey, 1 unchanged, up to 10), `brightness:-100-100`, `contrast:-100-100`, `hue:DEGREES`, `gamma:0.1-10`, `sepia` or `sepia:0-100`, `tint:COLOUR` (alpha sets strength), `opacity:0-100`, `pixelate:1-200`, `invert`.\n\n**Layers**: `overlay:FILE_ID` draws another image file of the same app on top (`~width:` and `~height:` in px or `pc` of the image; one keeps its shape, both fit it inside the box; `~transform:(CHAIN)` runs transforms on the overlay first). `text:TEXT` writes text, URL-encoded inside the transform (so encoded twice in the URL: `%2520` for a space), up to 500 characters (`~font:sans|sans-bold|sans-italic|mono|mono-bold`, `~size:PX|Npc` of the image height (default 32), `~color:`, `~background:` for a box, `~padding:PX`, `~wrap:PX|Npc`, `~align:left|center|right`). Both take `~position:` (default center), `~x:` and `~y:` (in from that edge, px or `pc`, may be negative), `~opacity:0-100`, `~rotate:DEGREES` and `~tile:true` with `~gap:` to repeat the layer as a pattern. At most 5 layers. An overlay must be a file of the same app (anything else is a 400 `invalid_transform`), an image of at most 25 MB; if it needs a signed URL, so does this request, even for a public image.\n\n**Output**: `format:auto|avif|webp|jpeg|png|tiff|bmp|json` (`auto` picks AVIF, then WebP, then the source format, from what `Accept` allows; images over 16 megapixels are sent as WebP instead of AVIF; `json` answers with the image's size instead of the image), `quality:1-100` (the same scale for JPEG, WebP and AVIF), `lossless` (lossless WebP or AVIF; `auto` keeps lossless chains on WebP), `background:COLOUR` (the default for later transforms that leave empty space), `strip` (re-encode without metadata), `palette` or `palette:json|css` (the image's main colours instead of the image, see the `Palette` schema; `~colors:0-16`, default 6, and for CSS `~prefix:NAME`). Whichever of `format` and `palette` comes last decides the output.\n\nA transform's options can follow its name when its main value is left out, as in `trim~color:white`.\n\nNumbers past a limit are clamped (resize to 4000px, blur to 100...). Colours are `hex(RGB)`, `hex(RRGGBB)`, `hex(RRGGBBAA)`, `rgb(R,G,B)`, `rgba(R,G,B,A)` or a CSS name. Empty space is transparent in PNG, WebP, AVIF and TIFF and white in JPEG and BMP unless a background is given.",
        "schema": {
          "type": "string",
          "maxLength": 2048
        },
        "examples": {
          "thumbnail": {
            "value": "resize:300x_~fit:cover;format:webp;quality:75"
          },
          "avatar": {
            "value": "resize:200x200~fit:contain~background:hex(fff)"
          },
          "responsiveAvatar": {
            "value": "smartCrop:96x96;radius:max;dpr:2;format:auto"
          },
          "avif": {
            "value": "resize:800x_;format:avif;quality:60"
          },
          "watermark": {
            "value": "resize:1600x_;overlay:3f2a9c1e-7b4d-4e8a-9c6f-1d2e3f4a5b6c~width:15pc~position:bottom-right~x:24~y:24~opacity:85"
          },
          "credit": {
            "value": "text:%C2%A9%20Jane%20Doe~size:4pc~color:white~background:hex(00000080)~padding:8~position:bottom-left~x:16~y:16"
          },
          "heroWithFocus": {
            "value": "resize:1200x600~focus:70pc,30pc;format:auto;quality:80"
          },
          "neverUpscaled": {
            "value": "resize:1600x_~upscale:false;format:auto"
          },
          "productShot": {
            "value": "trim;pad:24~background:white;resize:600x600~fit:contain~background:white"
          },
          "size": {
            "value": "resize:300x_;format:json"
          },
          "colours": {
            "value": "palette~colors:4"
          },
          "bannerColoursAsCss": {
            "value": "crop:100pcx20pc~position:top;palette:css~prefix:hero"
          },
          "card": {
            "value": "resizeAR:4x3~fit:contain~background:white;pad:16;border:1~color:hex(ddd)"
          }
        }
      }
    },
    "responses": {
      "Error": {
        "description": "An error. `error` is a short code and `message` says what went wrong.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "SignatureRequired": {
        "description": "The app requires signed reads, or the file's read permission is `authenticated`, and the request has no signature. In an app that requires signed reads the same answer is given whether or not the file exists.",
        "headers": {
          "WWW-Authenticate": {
            "schema": {
              "type": "string"
            },
            "description": "`FileboySignature realm=\"fileboy\"`"
          },
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "description": "Always `no-store`."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                }
              ],
              "examples": [
                {
                  "error": "signature_required",
                  "message": "This content needs a signed URL"
                }
              ]
            }
          }
        }
      },
      "SignatureInvalid": {
        "description": "The signature is wrong (`signature_invalid`: tampered, for another file, transform or filename, or made with another key), has expired (`signature_expired`) or the request comes from an address the URL is not valid for (`ip_not_allowed`).",
        "headers": {
          "Cache-Control": {
            "schema": {
              "type": "string"
            },
            "description": "Always `no-store`."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                }
              ],
              "examples": [
                {
                  "error": "signature_expired",
                  "message": "This signed URL has expired"
                }
              ]
            }
          }
        }
      },
      "Unauthorized": {
        "description": "No key was sent.",
        "content": {
          "application/json": {
            "schema": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/Error"
                }
              ],
              "examples": [
                {
                  "error": "unauthorized",
                  "message": "You are not authorized to perform this action"
                }
              ]
            }
          }
        }
      }
    },
    "schemas": {
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "created",
          "appId",
          "resource"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique id of the event. The same on every retry."
          },
          "type": {
            "type": "string",
            "enum": [
              "file.uploaded",
              "file.deleted",
              "video.uploaded",
              "video.ready",
              "video.failed",
              "video.deleted",
              "usage.threshold_reached",
              "webhook.test"
            ]
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "appId": {
            "type": "string"
          },
          "resource": {
            "type": "object",
            "description": "The file (`id`, `name`, `originalFilename`, `mime`, `fileSize`, `metadata`) or the video (`id`, `name`, `status`, `description`, `tags`, `metadata`, and on `video.ready` `duration`, `width`, `height` and `urls` with `manifest`, `hls`, `dash` and `poster`; on `video.failed` an `error`). `usage.threshold_reached` has the `UsageThresholdResource` fields. `webhook.test` has a `message`.",
            "additionalProperties": true
          }
        }
      },
      "UsageAlertThresholds": {
        "type": "array",
        "description": "Percentages of each limit, 1 to 100. At most five.",
        "items": {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        },
        "minItems": 1,
        "maxItems": 5,
        "examples": [
          [
            80,
            100
          ]
        ]
      },
      "UsageAlertRecipients": {
        "type": "array",
        "description": "Extra email addresses, up to 10.",
        "items": {
          "type": "string",
          "format": "email"
        },
        "maxItems": 10
      },
      "UsageAlertSettings": {
        "type": "object",
        "required": [
          "enabled",
          "thresholds",
          "metrics",
          "emailMembers",
          "recipients",
          "webhook"
        ],
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Off stops every alert for the app. On by default."
          },
          "thresholds": {
            "$ref": "#/components/schemas/UsageAlertThresholds"
          },
          "metrics": {
            "type": "object",
            "description": "Which limits are watched. All three by default.",
            "required": [
              "files",
              "videos",
              "storage"
            ],
            "properties": {
              "files": {
                "type": "boolean"
              },
              "videos": {
                "type": "boolean"
              },
              "storage": {
                "type": "boolean"
              }
            }
          },
          "emailMembers": {
            "type": "boolean",
            "description": "Email everyone who is a member of the app. On by default."
          },
          "recipients": {
            "$ref": "#/components/schemas/UsageAlertRecipients"
          },
          "webhook": {
            "type": "boolean",
            "description": "Send `usage.threshold_reached` to your webhook endpoints. On by default."
          }
        }
      },
      "UsageAlerts": {
        "type": "object",
        "required": [
          "settings",
          "period",
          "alerts"
        ],
        "properties": {
          "settings": {
            "$ref": "#/components/schemas/UsageAlertSettings"
          },
          "period": {
            "type": "object",
            "description": "The calendar month (UTC) alerts are counted in.",
            "required": [
              "key",
              "start",
              "end"
            ],
            "properties": {
              "key": {
                "type": "string",
                "examples": [
                  "2026-11"
                ]
              },
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "end": {
                "type": "string",
                "format": "date-time",
                "description": "Exclusive."
              }
            }
          },
          "alerts": {
            "type": "array",
            "description": "Alerts recorded this period, newest first.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "metric",
                "threshold",
                "used",
                "limit",
                "notified",
                "email"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "metric": {
                  "type": "string",
                  "enum": [
                    "files",
                    "videos",
                    "storage"
                  ]
                },
                "threshold": {
                  "type": "integer"
                },
                "used": {
                  "type": "integer",
                  "description": "Usage when it fired. Bytes for storage."
                },
                "limit": {
                  "type": "integer",
                  "description": "The limit then. Bytes for storage."
                },
                "notified": {
                  "type": "boolean",
                  "description": "False when a higher threshold was reached at the same time and only that one was sent."
                },
                "email": {
                  "type": "string",
                  "enum": [
                    "none",
                    "pending",
                    "sent",
                    "failed",
                    "skipped"
                  ],
                  "description": "`none` when email is off or the alert was not sent."
                },
                "createdAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "UsageThresholdResource": {
        "type": "object",
        "description": "The `resource` of a `usage.threshold_reached` webhook event.",
        "required": [
          "metric",
          "threshold",
          "used",
          "limit",
          "unit",
          "percent",
          "period",
          "plan"
        ],
        "properties": {
          "metric": {
            "type": "string",
            "enum": [
              "files",
              "videos",
              "storage"
            ]
          },
          "threshold": {
            "type": "integer",
            "description": "The percentage reached, like 80."
          },
          "used": {
            "type": "integer",
            "description": "Usage now. Bytes for storage."
          },
          "limit": {
            "type": "integer",
            "description": "The plan's limit. Bytes for storage."
          },
          "unit": {
            "type": "string",
            "enum": [
              "count",
              "bytes"
            ]
          },
          "percent": {
            "type": "number",
            "description": "Share of the limit used, one decimal place."
          },
          "period": {
            "type": "object",
            "required": [
              "start",
              "end"
            ],
            "properties": {
              "start": {
                "type": "string",
                "format": "date-time"
              },
              "end": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "plan": {
            "type": "string",
            "examples": [
              "Free",
              "Pro"
            ]
          },
          "test": {
            "type": "boolean",
            "description": "Only present, and true, on events sent with the dashboard's test button."
          }
        }
      },
      "AppSettings": {
        "type": "object",
        "required": [
          "requireSignedReads",
          "stripLocationOnUpload",
          "playerBranding"
        ],
        "properties": {
          "requireSignedReads": {
            "type": "boolean",
            "description": "When true, reading files, images and videos needs a signed URL or the private key."
          },
          "stripLocationOnUpload": {
            "type": "boolean",
            "description": "When true, the GPS position is removed from photos (EXIF and XMP) before they are stored."
          },
          "playerBranding": {
            "$ref": "#/components/schemas/PlayerBranding",
            "description": "The player branding every video starts with. `{}` when none is set."
          }
        }
      },
      "PlayerBranding": {
        "type": "object",
        "description": "How `<fileboy-player>` looks and behaves. Every field is optional. On a video, each field replaces the app's, except `controls`, which merge control by control.",
        "additionalProperties": false,
        "properties": {
          "accentColor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "The progress bar and hovered buttons. The player picks black or white icons on it and lightens it where it is text, so both stay readable. Null means not set."
          },
          "controlsColor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "Icons, the time and menu text."
          },
          "backgroundColor": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$",
            "description": "Around a video that doesn't fill the player."
          },
          "logo": {
            "description": "A logo over the video, or null for none (a video can drop the app's).",
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "fileId"
                ],
                "additionalProperties": false,
                "properties": {
                  "fileId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "An image in this app."
                  },
                  "position": {
                    "type": "string",
                    "enum": [
                      "top-left",
                      "top-right",
                      "bottom-left",
                      "bottom-right"
                    ],
                    "default": "top-right"
                  },
                  "link": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "An http or https URL, opened in a new tab."
                  },
                  "alt": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "The name screen readers read."
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "controls": {
            "type": "object",
            "description": "false hides a control and its keyboard shortcut (only m to mute always works). Every control shows by default except download, which offers the original file.",
            "additionalProperties": false,
            "properties": {
              "play": {
                "type": "boolean"
              },
              "seek": {
                "type": "boolean"
              },
              "volume": {
                "type": "boolean"
              },
              "time": {
                "type": "boolean"
              },
              "captions": {
                "type": "boolean"
              },
              "speed": {
                "type": "boolean"
              },
              "quality": {
                "type": "boolean"
              },
              "pip": {
                "type": "boolean"
              },
              "fullscreen": {
                "type": "boolean"
              },
              "download": {
                "type": "boolean"
              },
              "title": {
                "type": "boolean"
              }
            }
          },
          "autoplay": {
            "type": "boolean",
            "description": "Start playing, muted, on load."
          },
          "muted": {
            "type": "boolean"
          },
          "loop": {
            "type": "boolean"
          }
        }
      },
      "PlayerConfig": {
        "type": "object",
        "required": [
          "accentColor",
          "controlsColor",
          "backgroundColor",
          "logo",
          "controls",
          "downloadUrl",
          "autoplay",
          "muted",
          "loop"
        ],
        "properties": {
          "accentColor": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "#ff3e6c"
            ]
          },
          "controlsColor": {
            "type": [
              "string",
              "null"
            ]
          },
          "backgroundColor": {
            "type": [
              "string",
              "null"
            ]
          },
          "logo": {
            "oneOf": [
              {
                "type": "object",
                "required": [
                  "url",
                  "position",
                  "link",
                  "alt"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The logo image, resized for the player, signed when it needs to be."
                  },
                  "position": {
                    "type": "string",
                    "enum": [
                      "top-left",
                      "top-right",
                      "bottom-left",
                      "bottom-right"
                    ]
                  },
                  "link": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "alt": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "controls": {
            "type": "object",
            "description": "Every control, true when it shows.",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "downloadUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "The original file, when downloads are on."
          },
          "autoplay": {
            "type": "boolean"
          },
          "muted": {
            "type": "boolean"
          },
          "loop": {
            "type": "boolean"
          }
        }
      },
      "ExifGps": {
        "type": "object",
        "description": "Where the photo was taken. Personal data.",
        "required": [
          "latitude",
          "longitude",
          "altitude"
        ],
        "properties": {
          "latitude": {
            "type": "number",
            "description": "Decimal degrees, negative south of the equator."
          },
          "longitude": {
            "type": "number",
            "description": "Decimal degrees, negative west of Greenwich."
          },
          "altitude": {
            "type": [
              "number",
              "null"
            ],
            "description": "Metres above sea level, negative below."
          }
        }
      },
      "ExifFields": {
        "type": "object",
        "description": "The normalised metadata. A field is null when the file does not say. EXIF wins over XMP, and XMP over IPTC. `gps`, `city`, `state`, `country`, `sublocation`, `bodySerialNumber`, `lensSerialNumber` and `cameraOwnerName` are personal data.",
        "properties": {
          "make": {
            "type": [
              "string",
              "null"
            ]
          },
          "model": {
            "type": [
              "string",
              "null"
            ]
          },
          "lensMake": {
            "type": [
              "string",
              "null"
            ]
          },
          "lensModel": {
            "type": [
              "string",
              "null"
            ]
          },
          "focalLength": {
            "type": [
              "number",
              "null"
            ],
            "description": "Millimetres."
          },
          "focalLength35mm": {
            "type": [
              "number",
              "null"
            ],
            "description": "The 35 mm film equivalent."
          },
          "aperture": {
            "type": [
              "number",
              "null"
            ],
            "description": "The f-number: 2.8 for f/2.8."
          },
          "exposureTime": {
            "type": [
              "number",
              "null"
            ],
            "description": "Seconds."
          },
          "shutterSpeed": {
            "type": [
              "string",
              "null"
            ],
            "description": "The exposure written the usual way: \"1/250\", \"2\"."
          },
          "iso": {
            "type": [
              "integer",
              "null"
            ]
          },
          "exposureCompensation": {
            "type": [
              "number",
              "null"
            ],
            "description": "EV."
          },
          "exposureProgram": {
            "type": [
              "string",
              "null"
            ]
          },
          "meteringMode": {
            "type": [
              "string",
              "null"
            ]
          },
          "flashFired": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "whiteBalance": {
            "type": [
              "string",
              "null"
            ],
            "description": "\"Auto\" or \"Manual\"."
          },
          "dateTaken": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the photo was taken, ISO 8601, like \"2026-06-21T18:45:10.25+01:00\". It carries an offset only when the file records one; otherwise it is the camera's local time with no zone."
          },
          "timezone": {
            "type": [
              "string",
              "null"
            ],
            "description": "The recorded UTC offset of `dateTaken`, like \"+01:00\"."
          },
          "orientation": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "maximum": 8,
            "description": "EXIF orientation. Fileboy's transforms apply it."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "As the metadata records it."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "software": {
            "type": [
              "string",
              "null"
            ]
          },
          "colorSpace": {
            "type": [
              "string",
              "null"
            ],
            "description": "\"sRGB\", \"Adobe RGB\" or \"Uncalibrated\"."
          },
          "colorProfile": {
            "type": [
              "string",
              "null"
            ],
            "description": "The embedded ICC profile's name, like \"Display P3\"."
          },
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "artist": {
            "type": [
              "string",
              "null"
            ]
          },
          "copyright": {
            "type": [
              "string",
              "null"
            ]
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rating": {
            "type": [
              "integer",
              "null"
            ]
          },
          "gps": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/ExifGps"
              },
              {
                "type": "null"
              }
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "state": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          },
          "sublocation": {
            "type": [
              "string",
              "null"
            ]
          },
          "bodySerialNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "lensSerialNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "cameraOwnerName": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "FileExif": {
        "type": "object",
        "required": [
          "fileId",
          "status",
          "format",
          "extractedAt",
          "locationRemoved",
          "personalData",
          "fields",
          "raw",
          "warnings"
        ],
        "properties": {
          "fileId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "found",
              "none",
              "unsupported"
            ],
            "description": "`none`: an image with no metadata. `unsupported`: not an image format that is read."
          },
          "format": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "jpeg",
              "png",
              "webp",
              "tiff",
              "heif",
              "avif",
              null
            ]
          },
          "extractedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "locationRemoved": {
            "type": "boolean",
            "description": "The app's `stripLocationOnUpload` setting removed the GPS position at upload."
          },
          "personalData": {
            "type": "boolean",
            "description": "False when asked with `personal=false`."
          },
          "fields": {
            "$ref": "#/components/schemas/ExifFields"
          },
          "raw": {
            "type": "object",
            "description": "Every tag read, by tag name. EXIF names follow the EXIF specification (as ExifTool shows them), XMP keys are `prefix:Property` and IPTC keys the IIM dataset names. Rationals are decimals; the maker note and thumbnail are left out.",
            "required": [
              "exif",
              "iptc",
              "xmp"
            ],
            "properties": {
              "exif": {
                "type": "object",
                "additionalProperties": true
              },
              "iptc": {
                "type": "object",
                "additionalProperties": true
              },
              "xmp": {
                "type": "object",
                "additionalProperties": true
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Parts of the file that were malformed and skipped."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "A code such as `unauthorized`, `rate_limited`, `file_too_large`, `file_type_not_allowed`, `limit_reached`, `not_available_on_demo`, `bad_request`, `mime_type_error`, `url_fetch_error` or `upload_error`. Folder routes add `folder_not_found`, `folder_exists`, `folder_not_empty`, `folder_too_large`, `invalid_move`, `folder_too_deep`, `invalid_folder_name`, `invalid_folder_path` and `folder_limit_reached`. Some video routes send an empty string."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "UploadFileData": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "mime": {
            "type": "string",
            "examples": [
              "image/png"
            ]
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "folderId": {
            "type": "string",
            "format": "uuid",
            "description": "Put the file in this folder."
          },
          "folderPath": {
            "type": "string",
            "examples": [
              "/uploads/avatars"
            ],
            "description": "Put the file in the folder at this path. Signed with your private key, missing folders are created; with the public key the folder must exist."
          }
        }
      },
      "Metadata": {
        "type": "object",
        "description": "Your own string key and value pairs.",
        "additionalProperties": {
          "type": "string"
        }
      },
      "UploadFromUrl": {
        "type": "object",
        "required": [
          "url"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "An http or https address Fileboy can fetch."
          },
          "name": {
            "type": "string"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "folderId": {
            "type": "string",
            "format": "uuid",
            "description": "See `UploadFileData`."
          },
          "folderPath": {
            "type": "string",
            "description": "See `UploadFileData`."
          },
          "expect": {
            "description": "Reject the download unless it matches. A comma separated string or an array of `.ext`, `type/subtype`, `type/*` or `*/subtype` entries, like an HTML `accept` attribute.",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          }
        }
      },
      "File": {
        "type": "object",
        "required": [
          "id",
          "name",
          "mime",
          "fileSize",
          "hash",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "originalFilename": {
            "type": "string",
            "description": "The file name when it was uploaded, if known."
          },
          "mime": {
            "type": "string"
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "fileSize": {
            "type": "integer",
            "description": "Bytes."
          },
          "hash": {
            "type": "string",
            "description": "SHA-1 of the contents."
          },
          "signed": {
            "type": "boolean",
            "description": "Whether the upload was signed with a private key."
          },
          "diesAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the file is deleted automatically, if ever."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "folderId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The folder the file is in, null at the root. In upload and management responses, not on `/files/data/{id}`."
          },
          "folderPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "That folder's path, like `/products/shoes`. Null at the root."
          }
        }
      },
      "ManagedFile": {
        "allOf": [
          {
            "$ref": "#/components/schemas/File"
          },
          {
            "type": "object",
            "required": [
              "labels"
            ],
            "properties": {
              "labels": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Labels that point at this file."
              },
              "dominantColor": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The image's main colour as `#rrggbb`, for a placeholder while it loads. Null for files that are not images, and for older images until their palette is worked out.",
                "examples": [
                  "#3d5a73"
                ]
              },
              "sharePage": {
                "type": "boolean",
                "description": "Whether the file's share page (https://fileboy.io/f/{id}) is on. Off unless you turn it on."
              }
            }
          }
        ]
      },
      "FileShare": {
        "type": "object",
        "required": [
          "id",
          "name",
          "mime",
          "fileSize",
          "access",
          "urls"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "mime": {
            "type": "string"
          },
          "fileSize": {
            "type": "integer",
            "description": "Bytes."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "width": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pixels, for images."
          },
          "height": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Pixels, for images."
          },
          "access": {
            "type": "string",
            "enum": [
              "public",
              "signed"
            ],
            "description": "`signed` when the page was opened with a signed share link or the private key."
          },
          "expiresAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "When the signed link, and every URL in `urls`, stops working. Null for public files. With the private key it is an hour from the request."
          },
          "urls": {
            "type": "object",
            "required": [
              "view",
              "download",
              "images",
              "thumbnail"
            ],
            "properties": {
              "view": {
                "type": "string",
                "format": "uri",
                "description": "The file, shown inline."
              },
              "download": {
                "type": "string",
                "format": "uri",
                "description": "The file as an attachment, saved under its name."
              },
              "images": {
                "type": "array",
                "description": "Resized previews (WebP where the browser takes it) at up to 640, 1280, 1920 and 2560 pixels wide, never wider than the image, smallest first. For `srcset`. Empty for files the image service can't resize.",
                "items": {
                  "type": "object",
                  "required": [
                    "width",
                    "url"
                  ],
                  "properties": {
                    "width": {
                      "type": "integer"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              },
              "thumbnail": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "A JPEG at most 1200 pixels on its longest side, for link previews. Null for files the image service can't resize."
              }
            }
          }
        }
      },
      "FilePatch": {
        "type": "object",
        "description": "At least one field is needed. Unknown fields are rejected.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1024
          },
          "mime": {
            "type": "string",
            "examples": [
              "image/png"
            ]
          },
          "metadata": {
            "type": "object",
            "description": "Pairs to set. A `null` value removes the key. Up to 100 keys, keys up to 128 characters, values up to 4096. The stored metadata is at most 16 KB.",
            "additionalProperties": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          "replaceMetadata": {
            "type": "boolean",
            "default": false,
            "description": "Make `metadata` the complete new metadata instead of changes to the old. Values cannot be null."
          },
          "labels": {
            "type": "object",
            "properties": {
              "add": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "string"
                }
              },
              "remove": {
                "type": "array",
                "maxItems": 20,
                "items": {
                  "type": "string"
                }
              }
            }
          },
          "folderId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "Move the file to this folder, or to the root with null."
          },
          "folderPath": {
            "type": "string",
            "description": "Move the file to the folder at this path, creating missing folders. `/` is the root. Send `folderId` or `folderPath`, not both."
          },
          "sharePage": {
            "type": "boolean",
            "description": "Turn the file's share page on or off. A page that is on still follows the file's read access."
          }
        }
      },
      "Folder": {
        "type": "object",
        "required": [
          "id",
          "name",
          "path",
          "parentId",
          "fileCount",
          "folderCount",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "The names from the root, like `/products/shoes`."
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The folder it is in, null at the top level."
          },
          "fileCount": {
            "type": "integer",
            "description": "Files directly in this folder."
          },
          "folderCount": {
            "type": "integer",
            "description": "Folders directly in this folder."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ancestors": {
            "type": "array",
            "description": "The folders above this one, from the top. On single-folder responses only, not in lists.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "MoveFilesResult": {
        "type": "object",
        "required": [
          "moved",
          "notFound",
          "folder"
        ],
        "properties": {
          "moved": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "notFound": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Unknown ids and other apps' files."
          },
          "folder": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Folder"
              },
              {
                "type": "null"
              }
            ],
            "description": "The folder the files are in now, null for the root."
          }
        }
      },
      "FolderDeleteResult": {
        "type": "object",
        "required": [
          "deletedFolders",
          "deletedFiles",
          "movedFiles",
          "movedFolders"
        ],
        "properties": {
          "deletedFolders": {
            "type": "integer",
            "description": "The folder and any subfolders deleted."
          },
          "deletedFiles": {
            "type": "integer",
            "description": "Files deleted with `contents=delete`."
          },
          "movedFiles": {
            "type": "integer",
            "description": "Files moved up with `contents=moveToParent`."
          },
          "movedFolders": {
            "type": "integer",
            "description": "Subfolders moved up with `contents=moveToParent`."
          }
        }
      },
      "BulkDeleteResult": {
        "type": "object",
        "required": [
          "deleted",
          "notFound",
          "failed"
        ],
        "properties": {
          "deleted": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          },
          "notFound": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Unknown ids, other apps' files, and files that were already deleted."
          },
          "failed": {
            "type": "array",
            "description": "Files that are still there because storage could not be reached.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "message"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "FileLink": {
        "type": "object",
        "required": [
          "id",
          "fileId",
          "url",
          "status",
          "expiresAt",
          "useCount"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fileId": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The link to hand out, `https://api.fileboy.io/links/{token}`."
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "expired",
              "revoked"
            ]
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          },
          "revokedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "transform": {
            "type": "string",
            "description": "The image transform every read gets, empty for the file itself."
          },
          "download": {
            "type": "boolean",
            "description": "Sent as an attachment."
          },
          "filename": {
            "type": "string",
            "description": "The name it is saved under, empty for the file's own name."
          },
          "note": {
            "type": "string"
          },
          "createdVia": {
            "type": "string",
            "enum": [
              "api",
              "dashboard"
            ]
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "useCount": {
            "type": "integer",
            "description": "Reads that started at the beginning of the file. HEAD and later byte ranges are not counted."
          }
        }
      },
      "NewFileLink": {
        "type": "object",
        "additionalProperties": false,
        "description": "Give `expiresAt` or `expiresIn`, not both.",
        "properties": {
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the link stops working, with a time zone. A minute to 365 days away."
          },
          "expiresIn": {
            "type": "integer",
            "minimum": 60,
            "maximum": 31536000,
            "description": "Seconds from now."
          },
          "transform": {
            "type": "string",
            "maxLength": 2048,
            "description": "An image transform, like `resize:400x_;format:webp`. Checked when the link is made (400 `invalid_transform`)."
          },
          "download": {
            "type": "boolean",
            "default": false
          },
          "filename": {
            "type": "string",
            "maxLength": 255,
            "description": "The name the file is saved under. No slashes."
          },
          "note": {
            "type": "string",
            "maxLength": 200,
            "description": "For you: who or what the link is for."
          }
        }
      },
      "Label": {
        "type": "object",
        "required": [
          "id",
          "name",
          "fileId"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "fileId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid",
            "description": "The file the label points at, if any."
          }
        }
      },
      "VideoTags": {
        "type": "array",
        "description": "Up to 50 tags, each 1 to 64 characters with no commas or control characters. Case-sensitive, trimmed, and repeats are dropped.",
        "maxItems": 50,
        "items": {
          "type": "string",
          "minLength": 1,
          "maxLength": 64,
          "pattern": "^[^,]+$"
        }
      },
      "VideoPatch": {
        "type": "object",
        "description": "At least one field is needed. Unknown fields are rejected.",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1024
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 5000,
            "description": "Plain text, trimmed, line breaks kept. No control characters other than tabs and line breaks. `\"\"` or `null` removes it."
          },
          "tags": {
            "description": "A list replaces the tags, `null` removes them all, an object changes some.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/VideoTags"
              },
              {
                "type": "null"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "add": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string"
                    }
                  },
                  "remove": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            ]
          },
          "metadata": {
            "type": "object",
            "description": "Pairs to set. A `null` value removes the key. A video can have up to 100 keys of 1 to 128 characters, with no `[`, `]` or control characters, and values up to 4096 characters. The stored metadata is at most 16 KB.",
            "additionalProperties": {
              "type": [
                "string",
                "null"
              ]
            }
          },
          "replaceMetadata": {
            "type": "boolean",
            "default": false,
            "description": "Make `metadata` the complete new metadata instead of changes to the old. Values cannot be null."
          },
          "playerBranding": {
            "description": "This video's own player branding, on top of the app's. `null` makes the video follow the app again.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/PlayerBranding"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ManagedVideo": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "description",
          "tags",
          "metadata"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "processing",
              "ready",
              "failed"
            ]
          },
          "statusMessage": {
            "type": "string"
          },
          "duration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Seconds."
          },
          "width": {
            "type": [
              "integer",
              "null"
            ]
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          },
          "playCount": {
            "type": "integer"
          },
          "lastPlayedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "description": {
            "type": "string",
            "description": "Plain text shown on the watch page. Empty when there is none."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Private."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "thumbnails": {
            "$ref": "#/components/schemas/VideoThumbnails"
          },
          "urls": {
            "description": "Playable URLs. Null until the video is ready.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "manifest": {
                    "type": "string"
                  },
                  "hls": {
                    "type": "string"
                  },
                  "dash": {
                    "type": "string"
                  },
                  "poster": {
                    "type": "string"
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "playerBranding": {
            "$ref": "#/components/schemas/PlayerBranding",
            "description": "This video's own player branding, on top of the app's. `{}` when it follows the app."
          }
        }
      },
      "Usage": {
        "type": "object",
        "required": [
          "files",
          "videos",
          "storageBytes",
          "limits"
        ],
        "properties": {
          "files": {
            "type": "integer",
            "description": "Files you can manage, as `GET /files` lists."
          },
          "videos": {
            "type": "integer"
          },
          "storageBytes": {
            "type": "integer",
            "description": "Everything stored: files, image copies, video renditions, posters."
          },
          "limits": {
            "type": "object",
            "description": "The limits set on the app. Fileboy sets them from the app's plan, or from an agreement. Null means no limit is set on the app, not unlimited; the dashboard then shows the plan's standard limit.",
            "properties": {
              "files": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "videos": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "storageBytes": {
                "type": [
                  "integer",
                  "null"
                ]
              }
            }
          }
        }
      },
      "StorageKindUsage": {
        "type": "object",
        "required": [
          "count",
          "bytes"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Stored files of this kind."
          },
          "bytes": {
            "type": "integer",
            "description": "Their total size."
          }
        }
      },
      "StorageUsage": {
        "type": "object",
        "required": [
          "storageBytes",
          "limits",
          "kinds",
          "largestFiles",
          "largestVideos",
          "history"
        ],
        "properties": {
          "storageBytes": {
            "type": "integer",
            "description": "Everything stored, the same number as `GET /usage`."
          },
          "limits": {
            "type": "object",
            "properties": {
              "storageBytes": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "The storage limit set on the app, in bytes: the same value as `limits.storageBytes` in `GET /usage`. Fileboy sets it from the app's plan, or from an agreement. Null means no limit is set on the app, not unlimited; the dashboard then shows the plan's standard limit."
              }
            }
          },
          "kinds": {
            "type": "object",
            "description": "Every kind is present. They add up to `storageBytes`.",
            "required": [
              "images",
              "files",
              "videoOriginals",
              "videoRenditions",
              "videoPosters",
              "videoCaptions",
              "imageTransforms",
              "otherGenerated"
            ],
            "properties": {
              "images": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Images you uploaded."
              },
              "files": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Every other file you uploaded."
              },
              "videoOriginals": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "The file uploaded for each video."
              },
              "videoRenditions": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "The streaming versions made of each video."
              },
              "videoPosters": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Generated and custom posters."
              },
              "videoCaptions": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Caption and transcript files."
              },
              "imageTransforms": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Cached resized and converted copies of images and posters."
              },
              "otherGenerated": {
                "$ref": "#/components/schemas/StorageKindUsage",
                "description": "Anything else Fileboy generated."
              }
            }
          },
          "largestFiles": {
            "type": "array",
            "description": "Your biggest files, biggest first. Video parts and generated copies are not listed.",
            "items": {
              "$ref": "#/components/schemas/ManagedFile"
            }
          },
          "largestVideos": {
            "type": "array",
            "description": "The videos that store the most, biggest first.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "format": "uuid"
                },
                "name": {
                  "type": "string"
                },
                "createdAt": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "format": "date-time"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pending",
                    "processing",
                    "ready",
                    "failed"
                  ]
                },
                "storageBytes": {
                  "type": "integer",
                  "description": "Original, renditions, posters and captions together."
                },
                "originalBytes": {
                  "type": "integer"
                },
                "renditionBytes": {
                  "type": "integer"
                },
                "posterBytes": {
                  "type": "integer"
                },
                "captionBytes": {
                  "type": "integer"
                }
              }
            }
          },
          "history": {
            "type": "array",
            "description": "One total a day, oldest first.",
            "items": {
              "type": "object",
              "properties": {
                "date": {
                  "type": "string",
                  "format": "date",
                  "description": "The UTC day."
                },
                "storageBytes": {
                  "type": "integer"
                },
                "kinds": {
                  "type": "object",
                  "description": "Bytes by kind that day.",
                  "additionalProperties": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      },
      "ImageInfo": {
        "type": "object",
        "description": "What `format:json` answers with. Sizes are as the image is shown, after EXIF orientation.",
        "required": [
          "width",
          "height",
          "original"
        ],
        "properties": {
          "width": {
            "type": "integer",
            "description": "Width after the transforms."
          },
          "height": {
            "type": "integer",
            "description": "Height after the transforms."
          },
          "original": {
            "type": "object",
            "required": [
              "width",
              "height",
              "format",
              "bytes"
            ],
            "properties": {
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              },
              "format": {
                "type": "string",
                "examples": [
                  "image/jpeg"
                ]
              },
              "bytes": {
                "type": "integer",
                "description": "Size of the original file."
              }
            }
          }
        }
      },
      "PaletteSwatch": {
        "type": "object",
        "required": [
          "hex",
          "rgb",
          "share",
          "text",
          "contrast"
        ],
        "properties": {
          "hex": {
            "type": "string",
            "examples": [
              "#3d5a73"
            ]
          },
          "rgb": {
            "type": "array",
            "items": {
              "type": "integer",
              "minimum": 0,
              "maximum": 255
            },
            "minItems": 3,
            "maxItems": 3
          },
          "share": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "How much of the visible image is this colour."
          },
          "text": {
            "type": "string",
            "enum": [
              "#000000",
              "#ffffff"
            ],
            "description": "The text colour that contrasts more with this colour. It always meets WCAG AA for normal text (4.5:1)."
          },
          "contrast": {
            "type": "number",
            "description": "The WCAG 2 contrast ratio of `text` on this colour, 4.58 to 21."
          }
        }
      },
      "Palette": {
        "type": "object",
        "required": [
          "version",
          "dominant",
          "colors",
          "swatches",
          "transparentShare"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "description": "The version of the method that made it. Palettes made by an older version are worked out again."
          },
          "dominant": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/PaletteSwatch"
              },
              {
                "type": "null"
              }
            ],
            "description": "The colour that covers the most of the image. Null when nothing in it is visible."
          },
          "colors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaletteSwatch"
            },
            "maxItems": 16,
            "description": "The main colours, most of the image first."
          },
          "swatches": {
            "type": "object",
            "description": "Android Palette style swatches. Each is null when no colour in the image fits, and each colour is used once at most.",
            "properties": {
              "vibrant": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "darkVibrant": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "lightVibrant": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "muted": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "darkMuted": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "lightMuted": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/PaletteSwatch"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          },
          "transparentShare": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "How much of the image is transparent. Transparent pixels are left out of every share."
          }
        }
      },
      "VideoCreated": {
        "type": "object",
        "required": [
          "id",
          "name",
          "rawFileId"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata"
          },
          "rawFileId": {
            "type": "string",
            "format": "uuid",
            "description": "The stored original upload."
          }
        }
      },
      "VideoInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Plain text. Empty when there is none."
          },
          "rawVideoUrl": {
            "type": "string",
            "description": "Empty until the original is available."
          },
          "duration": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Seconds. Null while processing."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Video": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "description": "Plain text shown on the watch page. Empty when there is none. Tags and metadata are private, so they are not here."
          },
          "duration": {
            "type": [
              "number",
              "null"
            ],
            "description": "Seconds. Null while processing."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "playCount": {
            "type": "integer"
          },
          "lastPlayedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "posterFile": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FileRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "generatedPosterFile": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FileRef"
              },
              {
                "type": "null"
              }
            ]
          },
          "extractionStatus": {
            "type": "string",
            "description": "Processing state of the upload."
          },
          "extractionStartedAt": {
            "type": "string"
          },
          "statusMessage": {
            "type": "string"
          },
          "videoFiles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VideoFile"
            },
            "description": "Renditions up to 1000 pixels high."
          },
          "transcripts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TranscriptRef"
            }
          },
          "thumbnails": {
            "$ref": "#/components/schemas/VideoThumbnails"
          }
        }
      },
      "VideoThumbnails": {
        "type": "object",
        "description": "Seek-preview thumbnails. Fileboy makes them after it processes a video; `vtt` and `json` are set once `status` is `ready`. In `/videos/{id}/data` they carry the request's signature on.",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "none",
              "pending",
              "processing",
              "ready",
              "failed",
              "skipped"
            ],
            "description": "`none`: never made (an older video). `skipped`: nothing to make them from."
          },
          "vtt": {
            "type": "string",
            "description": "WebVTT thumbnails track."
          },
          "json": {
            "type": "string",
            "description": "The same as a JSON storyboard."
          }
        }
      },
      "ThumbnailStoryboard": {
        "type": "object",
        "required": [
          "duration",
          "interval",
          "count",
          "tileWidth",
          "tileHeight",
          "sheets",
          "tiles"
        ],
        "properties": {
          "duration": {
            "type": "number",
            "description": "Seconds."
          },
          "interval": {
            "type": "number",
            "description": "Seconds between frames."
          },
          "count": {
            "type": "integer",
            "description": "Number of tiles."
          },
          "tileWidth": {
            "type": "integer"
          },
          "tileHeight": {
            "type": "integer"
          },
          "columns": {
            "type": "integer",
            "description": "Tiles in a row of a sheet."
          },
          "format": {
            "type": "string",
            "enum": [
              "jpg"
            ]
          },
          "vtt": {
            "type": "string",
            "description": "The same thumbnails as WebVTT."
          },
          "sheets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "width": {
                  "type": "integer"
                },
                "height": {
                  "type": "integer"
                }
              }
            }
          },
          "tiles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "start": {
                  "type": "number",
                  "description": "Seconds."
                },
                "end": {
                  "type": "number",
                  "description": "Seconds."
                },
                "sheet": {
                  "type": "integer",
                  "description": "Index into sheets."
                },
                "x": {
                  "type": "integer"
                },
                "y": {
                  "type": "integer"
                }
              }
            }
          }
        }
      },
      "FileRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "VideoFile": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "width": {
            "type": [
              "number",
              "null"
            ]
          },
          "height": {
            "type": [
              "number",
              "null"
            ]
          },
          "processingStatus": {
            "type": "string"
          },
          "statusMessage": {
            "type": "string"
          }
        }
      },
      "TranscriptRef": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "language": {
            "type": "string"
          }
        }
      }
    }
  }
}
