{
  "info": {
    "_postman_id": "c02844be-6eff-56c4-92be-ea055fa97486",
    "name": "S5W — client API",
    "description": "# S5W — client API\n\nEverything an application needs from S5W: put a file in, get a link to it, deliver it.\nFull documentation: https://winterframe.net/en/apps/s5w/overview\n\n## Getting started — 3 steps\n\n1. **Import** this file into Postman — it holds everything needed.\n2. **Fill in two variables** of the collection: open the collection → **Variables** tab:\n\n| Variable | What it is | Example |\n|---|---|---|\n| `baseUrl` | the API address, scheme included, no trailing `/` | `https://s5.example.com` |\n| `token` | a bucket token from the admin panel, \"full access\" level | `s5w_9f3c…` |\n\n3. **Send \"0. Check the token\".** A `200` means everything is set.\n\n> **Put the token into \"Current value\"** and leave \"Initial value\" empty. The current value stays\n> on your machine and never gets exported — the collection can be shared safely.\n\n## How requests work\n\nEverything an application needs from S5W: put a file in, get a link to it, deliver it.\n\n**The bucket is decided by the token.** It is not part of any API address: a token is issued\nfor one bucket and works only there.\n\n**Token levels:** `BASIC` (\"delivery only\" in the panel) — `GET /v1/check` and downloads from\n`/p/{id}`; `FULL` (\"full access\") — everything.\n\n**Formats.** Lists answer `{ \"meta\": {…}, \"data\": [ … ] }` — except `GET /v1/files/{id}/links`,\nwhich answers an array. Errors answer `{ \"code\", \"message\" }`, plus per-field `errors` when\nvalidation failed; the exception is a body over 32 MiB: the server cuts it off before the\napplication, answering `413` with no body. Enumerations are sent as numbers in requests\n(`\"retention\": 3`), except the string `format`; responses carry them as an object\n`{ \"id\": 3, \"name\": \"MONTH\" }`. Deleting answers `204` with an empty body — except\n`DELETE /v1/links`: `200` and `{ \"epoch\" }`.\n\n**Time** is RFC 3339 (ISO 8601) with a time zone: `2026-11-08T17:10:44+00:00`. The same in\nevery response; standard libraries parse it as is.\n\n## Status codes\n\n| Code | When |\n|---|---|\n| `200` / `201` | success / created |\n| `204` | deleted, no body |\n| `400` | no file sent; an empty or too small upload chunk |\n| `401` | no token, or the token is unknown |\n| `403` | the token is disabled, expired, or its level is too low |\n| `404` | no such file, upload, link, or folder (on `/v1/folders/{name}`) |\n| `409` | conflict: the name is taken, the chunk offset does not match, the upload is not finished, the bucket is not active |\n| `413` | over the quota, a body over 32 MiB (no response body), a chunk over 16 MiB |\n| `416` | a `Range` outside the file |\n| `422` | validation failed, the given folder does not exist, the hash does not match, the image cannot be processed |\n| `501` | the server has no image processing or no encoder for the requested format |\n| `507` | the server is running out of disk space |\n\n## Running it all\n\nPress **Run** on the collection — the requests go top to bottom. Each one stores what the next\nneeds, and **\"8. Cleanup\"** deletes everything created — the run can be repeated. Only two requests\nof section 2 need a file from disk — pick it in the **Body** tab; without it only they fail.\n\nThe same API as an OpenAPI spec is `openapi.yaml` next to this file.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{token}}",
        "type": "string"
      }
    ]
  },
  "item": [
    {
      "name": "0. Check the token",
      "request": {
        "method": "GET",
        "header": [],
        "url": {
          "raw": "{{baseUrl}}/v1/check",
          "host": [
            "{{baseUrl}}"
          ],
          "path": [
            "v1",
            "check"
          ]
        },
        "description": "Checks the token and shows which bucket it belongs to, its access level and how much space is\nleft.\n\nUse it as the first request — to make sure the address and the token are right. In an\napplication — for example at start-up, to notice a revoked token at once.\n\n**Token:** `BASIC`\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The token is accepted. |\n| `401` | No token, or the token is unknown. |\n| `403` | The token is disabled, expired, or its level is too low. |"
      },
      "response": [
        {
          "name": "200 — success",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/check",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "check"
              ]
            }
          },
          "status": "OK",
          "code": 200,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json; charset=utf-8"
            },
            {
              "key": "Content-Length",
              "value": "176"
            }
          ],
          "body": "{\n  \"bucket\": {\n    \"id\": \"2faa480a-4f40-4131-b2ce-5cd6ced969a8\",\n    \"name\": \"postman\"\n  },\n  \"access\": {\n    \"id\": 2,\n    \"name\": \"FULL\"\n  },\n  \"expiresAt\": null,\n  \"bytes\": {\n    \"quota\": 104857600,\n    \"used\": 0,\n    \"free\": 104857600\n  }\n}"
        },
        {
          "name": "401 — no token",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/check",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "check"
              ]
            }
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "body": "{\n  \"code\": 401,\n  \"message\": \"Access token required\"\n}"
        },
        {
          "name": "401 — unknown token",
          "originalRequest": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/check",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "check"
              ]
            }
          },
          "status": "Unauthorized",
          "code": 401,
          "_postman_previewlanguage": "json",
          "header": [
            {
              "key": "Content-Type",
              "value": "application/json"
            }
          ],
          "body": "{\n  \"code\": 401,\n  \"message\": \"Access token is invalid\"\n}"
        }
      ],
      "event": [
        {
          "listen": "test",
          "script": {
            "type": "text/javascript",
            "exec": [
              "pm.test('token accepted', () => pm.response.to.have.status(200));",
              "const json = pm.response.json();",
              "pm.collectionVariables.set('bucketId', json.bucket.id);",
              "pm.test('the run needs a FULL token', () => pm.expect(json.access.name).to.eql('FULL'));"
            ]
          }
        }
      ]
    },
    {
      "name": "1. Folders",
      "description": "Optional grouping of files. A folder gives its files common rules — visibility, retention\nand caching. **The bucket root is always public**: keep private files in a folder with\n`public: false`.",
      "item": [
        {
          "name": "Create a folder",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/folders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders"
              ]
            },
            "description": "Creates a folder in the bucket. Folders do not nest.\n\nRetention counts from the upload of each file (from the move, for a file moved in); expired files\nare deleted by themselves, every 5 minutes.\n\n> Here the name is generated (`demo-…`) so the run can be repeated.\n\n**Token:** `FULL`\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | yes | Letters, digits, space, `.`, `_`, `-`; starts with a letter or a digit. |\n| `public` | boolean |  | `true` — files of the folder open at `/o/…` without a token. |\n| `retention` | `0` · `1` · `2` · `3` · `4` · `5` · `6` |  | Retention of the files. (`0` forever, `1` a day, `2` a week, `3` a month, `4` a quarter, `5` half a year, `6` a year) |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The folder is created. |\n| `401` | No token, or the token is unknown. |\n| `409` | Such a folder already exists, or the bucket is not active. |\n| `422` | The name does not fit. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 3\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 3\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "202"
                }
              ],
              "body": "{\n  \"name\": \"demo-1794090657532\",\n  \"public\": false,\n  \"retention\": {\n    \"id\": 3,\n    \"name\": \"MONTH\"\n  },\n  \"cache\": {\n    \"maxAge\": null,\n    \"visibility\": null\n  },\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "409 — folder already exists",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 3\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 409,\n  \"message\": \"Folder \\\"capture-dup\\\" already exists\"\n}"
            },
            {
              "name": "422 — invalid name",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 3\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Validation failed\",\n  \"errors\": {\n    \"name\": [\n      \"may contain letters, digits, space, dot, underscore, dash\"\n    ]\n  }\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.collectionVariables.set('folderName', 'demo-' + Date.now());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('folder created', () => pm.response.to.have.status(201));",
                  "pm.test('retention is a month', () => pm.expect(pm.response.json().retention.name).to.eql('MONTH'));"
                ]
              }
            }
          ]
        },
        {
          "name": "List folders",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/folders?page=1&limit=25",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                }
              ]
            },
            "description": "Returns the folders of the bucket page by page.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `page` | query |  | Page number, from 1. |\n| `limit` | query |  | Records per page. |\n| `search` | query |  | Search by name. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | A page of folders. |\n| `401` | No token, or the token is unknown. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders?page=1&limit=25",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders"
                  ],
                  "query": [
                    {
                      "key": "page",
                      "value": "1"
                    },
                    {
                      "key": "limit",
                      "value": "25"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "292"
                }
              ],
              "body": "{\n  \"meta\": {\n    \"current\": 1,\n    \"size\": 25,\n    \"total\": 1,\n    \"pages\": 1,\n    \"previous\": null,\n    \"next\": null\n  },\n  \"data\": [\n    {\n      \"name\": \"demo-1794090657532\",\n      \"public\": false,\n      \"retention\": {\n        \"id\": 3,\n        \"name\": \"MONTH\"\n      },\n      \"cache\": {\n        \"maxAge\": null,\n        \"visibility\": null\n      },\n      \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n      \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n    }\n  ]\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('list received', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get a folder",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders",
                "{{folderName}}"
              ]
            },
            "description": "Returns one folder by its name.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `name` | path | yes | Folder name. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The folder. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such folder. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders",
                    "{{folderName}}"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "202"
                }
              ],
              "body": "{\n  \"name\": \"demo-1794090657532\",\n  \"public\": false,\n  \"retention\": {\n    \"id\": 3,\n    \"name\": \"MONTH\"\n  },\n  \"cache\": {\n    \"maxAge\": null,\n    \"visibility\": null\n  },\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "404 — no such folder",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders",
                    "{{folderName}}"
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"Folder \\\"nope\\\" does not exist\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('folder found', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Update a folder",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders",
                "{{folderName}}"
              ]
            },
            "description": "Changes the name, visibility and retention. The body is the same as on creation; every field is\nset anew.\n\nThe change applies at once to the files already in the folder:\n\n- **visibility** changes for every file: close the folder and their `/o/…` stops working;\n- **retention** is recalculated for every file **from its upload date**. Shorten it and the\n  files older than the new term are deleted at the next sweep — within 5 minutes.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `name` | path | yes | Folder name. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | yes | Letters, digits, space, `.`, `_`, `-`; starts with a letter or a digit. |\n| `public` | boolean |  | `true` — files of the folder open at `/o/…` without a token. |\n| `retention` | `0` · `1` · `2` · `3` · `4` · `5` · `6` |  | Retention of the files. (`0` forever, `1` a day, `2` a week, `3` a month, `4` a quarter, `5` half a year, `6` a year) |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The folder. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such folder. |\n| `409` | The new name is taken. |\n| `422` | Validation failed. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 2\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders",
                    "{{folderName}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"{{folderName}}\",\n  \"public\": false,\n  \"retention\": 2\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "201"
                }
              ],
              "body": "{\n  \"name\": \"demo-1794090657532\",\n  \"public\": false,\n  \"retention\": {\n    \"id\": 2,\n    \"name\": \"WEEK\"\n  },\n  \"cache\": {\n    \"maxAge\": null,\n    \"visibility\": null\n  },\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('folder updated', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Set the folder cache policy",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/folders/{{folderName}}/cache",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders",
                "{{folderName}}",
                "cache"
              ]
            },
            "description": "Sets the `Cache-Control` header for the files of the folder — instead of the bucket policy. This is\na full replacement: both fields are set anew, and an omitted field becomes `null` too — inherit\nfrom the bucket.\n\nFor private files and the `/p`, `/t` channels the visibility is never above `PRIVATE`, and\n`max-age` never longer than what is left of the file or the link.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `name` | path | yes | Folder name. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `maxAge` | integer or null |  | Seconds to cache. |\n| `visibility` | `1` · `2` · `3` |  | Who caches. (`1` shared cache: browser and CDN, `2` browser only, `3` do not cache) |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The folder with the new `cache`. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such folder. |\n| `422` | Validation failed. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"maxAge\": 604800,\n  \"visibility\": 1\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders/{{folderName}}/cache",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders",
                    "{{folderName}}",
                    "cache"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"maxAge\": 604800,\n  \"visibility\": 1\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "223"
                }
              ],
              "body": "{\n  \"name\": \"demo-1794090657532\",\n  \"public\": false,\n  \"retention\": {\n    \"id\": 2,\n    \"name\": \"WEEK\"\n  },\n  \"cache\": {\n    \"maxAge\": 604800,\n    \"visibility\": {\n      \"id\": 1,\n      \"name\": \"SHARED\"\n    }\n  },\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('policy set', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "2. Single-request upload",
      "description": "For files up to **32 MiB**: one `multipart/form-data` request and the file is ready. Pick the file\nin the **Body** tab; without it the request answers `400`, but the run goes on.",
      "item": [
        {
          "name": "Upload a file",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files"
              ]
            },
            "description": "Uploads a file in one `multipart/form-data` request and returns its addresses at once. The\nlimit is **32 MiB** for the whole request; larger files go through the chunked upload.\n\n**The name** is unique within the folder: when taken, a number is added — `photo (1).png`. `/`,\n`\\` and control characters are replaced with `-`.\n\n**Deduplication.** When the bucket already holds this content, no disk space and no quota are\nspent, and the answer says `deduplicated: true`.\n\n**Image processing.** With `format`, `maxWidth`, `maxHeight`, `quality` the image is shrunk,\nrotated by EXIF and re-encoded before it is stored; the result is what is kept. Images are only\nshrunk, animation is left alone. Changing the format changes the extension too:\n`photo.png` → `photo.webp`.\n\n**Token:** `FULL`\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `file` | file | yes | The file itself. |\n| `folder` | string |  | Folder name. Without it — the bucket root, and the file is public. |\n| `name` | string |  | File name. Without it — the name of the uploaded file. |\n| `format` | `ORIGINAL` · `WEBP` · `JPEG` · `PNG` · `AVIF` |  | Output format; `ORIGINAL` — keep. |\n| `maxWidth` | integer |  | Shrink to this width, keeping proportions. |\n| `maxHeight` | integer |  | Shrink to this height, keeping proportions. |\n| `quality` | integer |  | Quality; without it — by format: AVIF 55, PNG 60, WEBP and JPEG 82. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The file is uploaded. |\n| `400` | No file was sent. |\n| `401` | No token, or the token is unknown. |\n| `403` | The token is disabled, expired, or its level is too low. |\n| `413` | Over the bucket quota. A request over 32 MiB gets 413 too, but with no body: the server cuts it off. |\n| `501` | No image processing, or no encoder for the requested format. |\n| `422` | The given folder does not exist, validation failed, or the image cannot be processed. |",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "description": "A file from disk — pick it here",
                  "src": []
                },
                {
                  "key": "name",
                  "value": "report.pdf",
                  "type": "text",
                  "description": "File name (optional)",
                  "disabled": true
                },
                {
                  "key": "folder",
                  "value": "{{folderName}}",
                  "type": "text",
                  "description": "Folder name (optional)",
                  "disabled": true
                }
              ]
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "description": "A file from disk — pick it here",
                      "src": []
                    },
                    {
                      "key": "name",
                      "value": "report.pdf",
                      "type": "text",
                      "description": "File name (optional)",
                      "disabled": true
                    },
                    {
                      "key": "folder",
                      "value": "{{folderName}}",
                      "type": "text",
                      "description": "Folder name (optional)",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "525"
                }
              ],
              "body": "{\n  \"id\": \"60I4PpbPsXVagANk\",\n  \"name\": \"photo.png\",\n  \"folder\": null,\n  \"content\": {\n    \"size\": 4654,\n    \"mime\": \"image/png\",\n    \"extension\": \"png\",\n    \"hash\": \"ee300dd3885906657a97edefa78057610a91570d6e594a764fab92c6db518572\"\n  },\n  \"public\": true,\n  \"deduplicated\": false,\n  \"processed\": {\n    \"applied\": false,\n    \"reason\": \"no options\"\n  },\n  \"expiresAt\": null,\n  \"privateUrl\": \"https://s5.example.com/p/60I4PpbPsXVagANk\",\n  \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/60I4PpbPsXVagANk\",\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "400 — no file picked",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "description": "A file from disk — pick it here",
                      "src": []
                    },
                    {
                      "key": "name",
                      "value": "report.pdf",
                      "type": "text",
                      "description": "File name (optional)",
                      "disabled": true
                    },
                    {
                      "key": "folder",
                      "value": "{{folderName}}",
                      "type": "text",
                      "description": "Folder name (optional)",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 400,\n  \"message\": \"Uploaded file 'file' is missing\"\n}"
            },
            {
              "name": "422 — no such folder",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "description": "A file from disk — pick it here",
                      "src": []
                    },
                    {
                      "key": "name",
                      "value": "report.pdf",
                      "type": "text",
                      "description": "File name (optional)",
                      "disabled": true
                    },
                    {
                      "key": "folder",
                      "value": "{{folderName}}",
                      "type": "text",
                      "description": "Folder name (optional)",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Folder \\\"nope\\\" does not exist\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) pm.collectionVariables.set('uploadedFileId', pm.response.json().id);",
                  "pm.test('file uploaded (if picked in the Body tab)', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ]
        },
        {
          "name": "Upload an image with processing",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files"
              ]
            },
            "description": "Uploads a file in one `multipart/form-data` request and returns its addresses at once. The\nlimit is **32 MiB** for the whole request; larger files go through the chunked upload.\n\n**The name** is unique within the folder: when taken, a number is added — `photo (1).png`. `/`,\n`\\` and control characters are replaced with `-`.\n\n**Deduplication.** When the bucket already holds this content, no disk space and no quota are\nspent, and the answer says `deduplicated: true`.\n\n**Image processing.** With `format`, `maxWidth`, `maxHeight`, `quality` the image is shrunk,\nrotated by EXIF and re-encoded before it is stored; the result is what is kept. Images are only\nshrunk, animation is left alone. Changing the format changes the extension too:\n`photo.png` → `photo.webp`.\n\nHere the image is re-encoded to `webp` and shrunk to 800 px wide. `processed` in the answer shows what was done.\n\n**Token:** `FULL`\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `file` | file | yes | The file itself. |\n| `folder` | string |  | Folder name. Without it — the bucket root, and the file is public. |\n| `name` | string |  | File name. Without it — the name of the uploaded file. |\n| `format` | `ORIGINAL` · `WEBP` · `JPEG` · `PNG` · `AVIF` |  | Output format; `ORIGINAL` — keep. |\n| `maxWidth` | integer |  | Shrink to this width, keeping proportions. |\n| `maxHeight` | integer |  | Shrink to this height, keeping proportions. |\n| `quality` | integer |  | Quality; without it — by format: AVIF 55, PNG 60, WEBP and JPEG 82. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The file is uploaded. |\n| `400` | No file was sent. |\n| `401` | No token, or the token is unknown. |\n| `403` | The token is disabled, expired, or its level is too low. |\n| `413` | Over the bucket quota. A request over 32 MiB gets 413 too, but with no body: the server cuts it off. |\n| `501` | No image processing, or no encoder for the requested format. |\n| `422` | The given folder does not exist, validation failed, or the image cannot be processed. |",
            "body": {
              "mode": "formdata",
              "formdata": [
                {
                  "key": "file",
                  "type": "file",
                  "description": "An image: jpeg, png, gif, webp or avif",
                  "src": []
                },
                {
                  "key": "format",
                  "value": "WEBP",
                  "type": "text",
                  "description": "ORIGINAL | WEBP | JPEG | PNG | AVIF"
                },
                {
                  "key": "maxWidth",
                  "value": "800",
                  "type": "text",
                  "description": "Shrink to width, 16–10000"
                },
                {
                  "key": "quality",
                  "value": "80",
                  "type": "text",
                  "description": "Quality 1–100 (optional)",
                  "disabled": true
                }
              ]
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "description": "An image: jpeg, png, gif, webp or avif",
                      "src": []
                    },
                    {
                      "key": "format",
                      "value": "WEBP",
                      "type": "text",
                      "description": "ORIGINAL | WEBP | JPEG | PNG | AVIF"
                    },
                    {
                      "key": "maxWidth",
                      "value": "800",
                      "type": "text",
                      "description": "Shrink to width, 16–10000"
                    },
                    {
                      "key": "quality",
                      "value": "80",
                      "type": "text",
                      "description": "Quality 1–100 (optional)",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "690"
                }
              ],
              "body": "{\n  \"id\": \"0FwMWr8RLbjRUCfD\",\n  \"name\": \"photo.webp\",\n  \"folder\": null,\n  \"content\": {\n    \"size\": 2250,\n    \"mime\": \"image/webp\",\n    \"extension\": \"webp\",\n    \"hash\": \"250c33b15669b8b0ceda27d301f46414e4e399ec9f85d9b5f9d34eeddb104381\"\n  },\n  \"public\": true,\n  \"deduplicated\": false,\n  \"processed\": {\n    \"applied\": true,\n    \"operations\": [\n      \"resize:800x533\",\n      \"encode:webp@82\"\n    ],\n    \"source\": {\n      \"width\": 1200,\n      \"height\": 800,\n      \"size\": 4654,\n      \"mime\": \"image/png\"\n    },\n    \"result\": {\n      \"width\": 800,\n      \"height\": 533,\n      \"size\": 2250,\n      \"mime\": \"image/webp\"\n    }\n  },\n  \"expiresAt\": null,\n  \"privateUrl\": \"https://s5.example.com/p/0FwMWr8RLbjRUCfD\",\n  \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/0FwMWr8RLbjRUCfD\",\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "422 — image options on a non-image",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ]
                },
                "body": {
                  "mode": "formdata",
                  "formdata": [
                    {
                      "key": "file",
                      "type": "file",
                      "description": "An image: jpeg, png, gif, webp or avif",
                      "src": []
                    },
                    {
                      "key": "format",
                      "value": "WEBP",
                      "type": "text",
                      "description": "ORIGINAL | WEBP | JPEG | PNG | AVIF"
                    },
                    {
                      "key": "maxWidth",
                      "value": "800",
                      "type": "text",
                      "description": "Shrink to width, 16–10000"
                    },
                    {
                      "key": "quality",
                      "value": "80",
                      "type": "text",
                      "description": "Quality 1–100 (optional)",
                      "disabled": true
                    }
                  ]
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Image options were given, but this file is not an image\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (pm.response.code === 201) pm.collectionVariables.set('imageFileId', pm.response.json().id);",
                  "pm.test('image uploaded (if picked in the Body tab)', () => pm.response.to.have.status(201));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "3. Chunked upload",
      "description": "For large files and unreliable networks: the file goes in chunks of 4–16 MiB and resumes\nfrom what arrived after a break. Order: start → send chunks → complete.",
      "item": [
        {
          "name": "1. Start an upload",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads"
              ]
            },
            "description": "Opens an upload: the server checks the quota and the free disk space up front and returns the\nupload `id` and the recommended chunk size `chunkSize`. The upload waits 24 hours after the last\naccepted chunk; every chunk extends the term.\n\n**Deduplication by `sha256`.** When the bucket already holds this content, the file is created at\nonce: the answer has `chunkSize: 0` and a ready `file` with `deduplicated: true` — no chunks and\nno completion are needed. With image options the hash lookup is skipped: the hash is of the\nsource, while the processed result is what gets stored.\n\n**Token:** `FULL`\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | yes | Name of the future file. |\n| `size` | integer | yes | File size, bytes. |\n| `sha256` | string |  | Content hash, 64 hex characters: integrity check on completion and deduplication. |\n| `folder` | string |  | Folder name. Checked at once: `422` when it does not exist. |\n| `format` | `ORIGINAL` · `WEBP` · `JPEG` · `PNG` · `AVIF` |  | Output format; `ORIGINAL` — keep. |\n| `maxWidth` | integer |  | Shrink to this width, keeping proportions. |\n| `maxHeight` | integer |  | Shrink to this height, keeping proportions. |\n| `quality` | integer |  | Quality; without it — by format: AVIF 55, PNG 60, WEBP and JPEG 82. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The upload is open — or the file was created at once by `sha256`. |\n| `401` | No token, or the token is unknown. |\n| `413` | Over the bucket quota. |\n| `422` | The given folder does not exist, or validation failed. |\n| `507` | Less than 512 MiB would be left on the server disk. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"notes.txt\",\n  \"size\": {{chunkSize}},\n  \"sha256\": \"{{contentHash}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes.txt\",\n  \"size\": {{chunkSize}},\n  \"sha256\": \"{{contentHash}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "157"
                }
              ],
              "body": "{\n  \"id\": \"b7336f5b-8896-4e90-96e0-df39f22a115a\",\n  \"name\": \"notes.txt\",\n  \"size\": 66,\n  \"offset\": 0,\n  \"chunkSize\": 8388608,\n  \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n  \"file\": null\n}"
            },
            {
              "name": "413 — over the quota",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes.txt\",\n  \"size\": {{chunkSize}},\n  \"sha256\": \"{{contentHash}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Payload Too Large",
              "code": 413,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 413,\n  \"message\": \"Bucket quota exceeded: this upload needs 1000000000000 bytes, 104857600 of 104857600 bytes are free\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "// In this example the \"file\" is a short text, so the run needs no file picked.",
                  "// In an app these are the bytes of a real file; size and sha256 are computed from them.",
                  "const text = 'Hello from S5W! This content was uploaded in chunks. ' + Date.now();",
                  "const crypto = require('crypto-js');",
                  "pm.collectionVariables.set('chunkText', text);",
                  "pm.collectionVariables.set('chunkSize', crypto.enc.Utf8.parse(text).sigBytes);",
                  "pm.collectionVariables.set('contentHash', crypto.SHA256(text).toString());"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('upload opened', () => pm.response.to.have.status(201));",
                  "const json = pm.response.json();",
                  "pm.collectionVariables.set('uploadId', json.id);",
                  "pm.collectionVariables.set('uploadOffset', json.offset);"
                ]
              }
            }
          ]
        },
        {
          "name": "2. Send a chunk",
          "request": {
            "method": "PATCH",
            "header": [
              {
                "key": "Upload-Offset",
                "value": "{{uploadOffset}}"
              },
              {
                "key": "Content-Type",
                "value": "application/offset+octet-stream"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads",
                "{{uploadId}}"
              ]
            },
            "description": "Appends the next chunk to the upload. **The body is the raw bytes of the chunk** — not JSON, not\na form. Repeat until `offset` in the answer equals `size`.\n\nA chunk is 4 to 16 MiB; only the last one may be smaller. Send `Upload-Offset`: when a chunk is\nlost or sent twice, the server answers `409` instead of producing a corrupted file.\n\n> In this example the whole \"upload\" fits in one chunk — it is the last one and may be small.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | Upload id from `POST /v1/uploads`. |\n| `Upload-Offset` | header |  | The byte this chunk starts at. Optional, but worth sending. |\n\n**Request body:** raw bytes — The chunk bytes, 4–16 MiB (the last one — any size).\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The chunk is accepted; `offset` is the new position. |\n| `400` | The chunk is empty, or under 4 MiB and not the last. |\n| `401` | No token, or the token is unknown. |\n| `404` | The upload does not exist or has expired. |\n| `409` | `Upload-Offset` does not match what has arrived. |\n| `413` | The chunk is over 16 MiB or past the declared `size`. |",
            "body": {
              "mode": "raw",
              "raw": "{{chunkText}}",
              "options": {
                "raw": {
                  "language": "text"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Upload-Offset",
                    "value": "{{uploadOffset}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/offset+octet-stream"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{{chunkText}}",
                  "options": {
                    "raw": {
                      "language": "text"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "158"
                }
              ],
              "body": "{\n  \"id\": \"b7336f5b-8896-4e90-96e0-df39f22a115a\",\n  \"name\": \"notes.txt\",\n  \"size\": 66,\n  \"offset\": 66,\n  \"chunkSize\": 8388608,\n  \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n  \"file\": null\n}"
            },
            {
              "name": "409 — offset mismatch",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Upload-Offset",
                    "value": "{{uploadOffset}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/offset+octet-stream"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{{chunkText}}",
                  "options": {
                    "raw": {
                      "language": "text"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 409,\n  \"message\": \"Chunk offset 4096 does not continue this upload at 0\"\n}"
            },
            {
              "name": "400 — chunk under 4 MiB and not last",
              "originalRequest": {
                "method": "PATCH",
                "header": [
                  {
                    "key": "Upload-Offset",
                    "value": "{{uploadOffset}}"
                  },
                  {
                    "key": "Content-Type",
                    "value": "application/offset+octet-stream"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{{chunkText}}",
                  "options": {
                    "raw": {
                      "language": "text"
                    }
                  }
                }
              },
              "status": "Bad Request",
              "code": 400,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 400,\n  \"message\": \"Chunk is too small: 10 bytes, the minimum is 4194304\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('chunk accepted', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.collectionVariables.set('uploadOffset', json.offset);",
                  "pm.test('everything arrived — ready to complete', () => pm.expect(json.offset).to.eql(json.size));"
                ]
              }
            }
          ]
        },
        {
          "name": "3. Check what arrived",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads",
                "{{uploadId}}"
              ]
            },
            "description": "The state of the upload: `offset` is how many bytes the server has. After a break, continue from there while the upload has not expired (24 hours after the last chunk).\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | Upload id from `POST /v1/uploads`. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The upload. |\n| `401` | No token, or the token is unknown. |\n| `404` | The upload does not exist or has expired. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "158"
                }
              ],
              "body": "{\n  \"id\": \"b7336f5b-8896-4e90-96e0-df39f22a115a\",\n  \"name\": \"notes.txt\",\n  \"size\": 66,\n  \"offset\": 66,\n  \"chunkSize\": 8388608,\n  \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n  \"file\": null\n}"
            },
            {
              "name": "404 — no such upload",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}"
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"Upload does not exist or has expired\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('state received', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "4. Complete the upload",
          "request": {
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}/complete",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads",
                "{{uploadId}}",
                "complete"
              ]
            },
            "description": "Closes the upload and creates the file: checks the hash, processes the image when asked, moves the\ncontent into storage.\n\n**Any completion error deletes the upload** — hash mismatch, the quota ran out, the folder was\ndeleted, the image cannot be processed: the upload starts over. The exception is `409` \"not\neverything arrived\": the upload stays and can be finished.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | Upload id from `POST /v1/uploads`. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The file is created. |\n| `401` | No token, or the token is unknown. |\n| `404` | The upload does not exist or has expired. |\n| `409` | Not everything has arrived — the upload stays; or the data on disk is shorter than declared — the upload is deleted. |\n| `413` | The bucket quota ran out by completion; the upload is deleted. |\n| `422` | The content does not match `sha256`, the folder was deleted, or the image cannot be processed; the upload is deleted. |\n| `501` | No image processing or no encoder for the format; the upload is deleted. |"
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}/complete",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}",
                    "complete"
                  ]
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "524"
                }
              ],
              "body": "{\n  \"id\": \"tT3q5_AMMrYnVEPp\",\n  \"name\": \"notes.txt\",\n  \"folder\": null,\n  \"content\": {\n    \"size\": 66,\n    \"mime\": \"text/plain\",\n    \"extension\": \"txt\",\n    \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n  },\n  \"public\": true,\n  \"deduplicated\": false,\n  \"processed\": {\n    \"applied\": false,\n    \"reason\": \"no options\"\n  },\n  \"expiresAt\": null,\n  \"privateUrl\": \"https://s5.example.com/p/tT3q5_AMMrYnVEPp\",\n  \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/tT3q5_AMMrYnVEPp\",\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "409 — not everything arrived",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}/complete",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}",
                    "complete"
                  ]
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 409,\n  \"message\": \"Upload is not finished: 0 bytes of 10485760 received\"\n}"
            },
            {
              "name": "422 — hash mismatch",
              "originalRequest": {
                "method": "POST",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{uploadId}}/complete",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{uploadId}}",
                    "complete"
                  ]
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Uploaded data does not match the declared checksum\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file created', () => pm.response.to.have.status(201));",
                  "const json = pm.response.json();",
                  "pm.collectionVariables.set('fileId', json.id);",
                  "pm.test('hash matches the one sent', () => pm.expect(json.content.hash).to.eql(pm.collectionVariables.get('contentHash')));"
                ]
              }
            }
          ]
        },
        {
          "name": "Skip the transfer when the file is already there",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads"
              ]
            },
            "description": "The same \"Start an upload\" request, but with the hash of content the bucket already holds (from\nstep 1). The server creates the file on it at once — **without sending the bytes**.\n\n**How to tell it worked:** `chunkSize: 0`, and `file` holds a ready file with `deduplicated: true`.\nNo chunks and no completion are needed. Without such content the answer is as in step 1.\n\nWith image options the hash lookup is skipped.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"notes-copy.txt\",\n  \"size\": {{chunkSize}},\n  \"sha256\": \"{{contentHash}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes-copy.txt\",\n  \"size\": {{chunkSize}},\n  \"sha256\": \"{{contentHash}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "585"
                }
              ],
              "body": "{\n  \"id\": \"\",\n  \"name\": \"notes-copy.txt\",\n  \"size\": 66,\n  \"offset\": 66,\n  \"chunkSize\": 0,\n  \"expiresAt\": \"\",\n  \"file\": {\n    \"id\": \"cZtMNnijaQ3Cy1OF\",\n    \"name\": \"notes-copy.txt\",\n    \"folder\": null,\n    \"content\": {\n      \"size\": 66,\n      \"mime\": \"text/plain\",\n      \"extension\": \"txt\",\n      \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n    },\n    \"public\": true,\n    \"deduplicated\": true,\n    \"processed\": null,\n    \"expiresAt\": null,\n    \"privateUrl\": \"https://s5.example.com/p/cZtMNnijaQ3Cy1OF\",\n    \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/cZtMNnijaQ3Cy1OF\",\n    \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n    \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n  }\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('answered', () => pm.response.to.have.status(201));",
                  "const json = pm.response.json();",
                  "pm.test('no bytes to send', () => pm.expect(json.chunkSize).to.eql(0));",
                  "pm.test('file created on existing content', () => pm.expect(json.file.deduplicated).to.eql(true));",
                  "pm.collectionVariables.set('dedupFileId', json.file.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Start an upload to cancel",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads"
              ]
            },
            "description": "Setup for the next request: opens an upload for it to cancel.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"cancel-me.bin\",\n  \"size\": 10485760\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"cancel-me.bin\",\n  \"size\": 10485760\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "167"
                }
              ],
              "body": "{\n  \"id\": \"27ee7332-4cb4-4712-9818-598b4eb12de5\",\n  \"name\": \"cancel-me.bin\",\n  \"size\": 10485760,\n  \"offset\": 0,\n  \"chunkSize\": 8388608,\n  \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n  \"file\": null\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('upload opened', () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set('cancelUploadId', pm.response.json().id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Cancel an upload",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/uploads/{{cancelUploadId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "uploads",
                "{{cancelUploadId}}"
              ]
            },
            "description": "Cancels the upload and deletes what arrived at once. An abandoned upload is deleted anyway: 24 hours after the last chunk, the sweeper removes it within an hour.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | Upload id from `POST /v1/uploads`. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `204` | Cancelled, no body. |\n| `401` | No token, or the token is unknown. |\n| `404` | The upload does not exist or has expired. |"
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/uploads/{{cancelUploadId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "uploads",
                    "{{cancelUploadId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('upload cancelled', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "4. Delivery",
      "description": "How a browser, an app or a CDN gets the file: publicly (`/o`), with a token (`/p`) and through\na temporary link (`/t`). All three support `ETag` and conditional requests; `Range` too, except a\ntemporary link with a download limit.",
      "item": [
        {
          "name": "Download a public file · /o",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "o",
                "{{bucketId}}",
                "{{fileId}}"
              ]
            },
            "description": "Serves a public file **without a token** — the address from `publicUrl`. Put it into\n`<img src>`, hand it to users and to a CDN. Works only for public files — in the bucket root or\nin a folder with `public: true`; others answer `404`.\n\nImages, video, audio, PDF and plain text open in the browser, everything else downloads; HTML\nand SVG always download. `?download=1` asks for a download in any case.\n\n**Token:** not needed\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `bucketId` | path | yes | Bucket id. |\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n| `download` | query |  | `1` — download (`Content-Disposition: attachment`) instead of opening. |\n| `Range` | header |  | A byte range, e.g. `bytes=0-1023` — answers `206`; outside the file — `416`. Ignored on a temporary link with a download limit. |\n| `If-None-Match` | header |  | An `ETag` received before: when the file has not changed — `304` with no body. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file content. Headers: `Content-Type`, `Content-Length`, `ETag`, `Cache-Control`, `Content-Disposition`, `Accept-Ranges`. |\n| `206` | The requested byte range; `Content-Range` header. |\n| `304` | The file has not changed since that `ETag` — no body. |\n| `404` | The file is not public or does not exist. |\n| `416` | The `Range` is outside the file. |",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "o",
                    "{{bucketId}}",
                    "{{fileId}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "public, max-age=86400, immutable"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Disposition",
                  "value": "inline; filename=\"notes.txt\"; filename*=UTF-8''notes.txt"
                },
                {
                  "key": "Content-Length",
                  "value": "66"
                }
              ],
              "body": ""
            },
            {
              "name": "404 — not public or missing",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "o",
                    "{{bucketId}}",
                    "{{fileId}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"File not found\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file served', () => pm.response.to.have.status(200));",
                  "pm.test('ETag present', () => pm.response.to.have.header('ETag'));",
                  "pm.collectionVariables.set('etag', pm.response.headers.get('ETag'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Check whether the file changed · ETag",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "If-None-Match",
                "value": "{{etag}}"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "o",
                "{{bucketId}}",
                "{{fileId}}"
              ]
            },
            "description": "A conditional request: the `ETag` from an earlier answer in `If-None-Match`. Unchanged file — `304` with no body, no traffic spent. Browsers and CDNs do this themselves.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "304 — success",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "If-None-Match",
                    "value": "{{etag}}"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "o",
                    "{{bucketId}}",
                    "{{fileId}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "Not Modified",
              "code": 304,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "public, max-age=86400, immutable"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('unchanged — 304', () => pm.response.to.have.status(304));"
                ]
              }
            }
          ]
        },
        {
          "name": "Download part of a file · Range",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "Range",
                "value": "bytes=0-9"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "o",
                "{{bucketId}}",
                "{{fileId}}"
              ]
            },
            "description": "Only the requested byte range — `206 Partial Content`. This is how resuming and video seeking work.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "206 — success",
              "originalRequest": {
                "method": "GET",
                "header": [
                  {
                    "key": "Range",
                    "value": "bytes=0-9"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "o",
                    "{{bucketId}}",
                    "{{fileId}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "Partial Content",
              "code": 206,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "public, max-age=86400, immutable"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Disposition",
                  "value": "inline; filename=\"notes.txt\"; filename*=UTF-8''notes.txt"
                },
                {
                  "key": "Content-Length",
                  "value": "10"
                },
                {
                  "key": "Content-Range",
                  "value": "bytes 0-9/66"
                }
              ],
              "body": "Hello from"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('partial — 206', () => pm.response.to.have.status(206));",
                  "pm.test('exactly 10 bytes', () => pm.expect(pm.response.stream.length).to.eql(10));"
                ]
              }
            }
          ]
        },
        {
          "name": "Download instead of opening · ?download=1",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}?download=1",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "o",
                "{{bucketId}}",
                "{{fileId}}"
              ],
              "query": [
                {
                  "key": "download",
                  "value": "1"
                }
              ]
            },
            "description": "Asks the browser to save the file: `Content-Disposition: attachment`. Works on `/o` and `/p`.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/o/{{bucketId}}/{{fileId}}?download=1",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "o",
                    "{{bucketId}}",
                    "{{fileId}}"
                  ],
                  "query": [
                    {
                      "key": "download",
                      "value": "1"
                    }
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "public, max-age=86400, immutable"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Disposition",
                  "value": "attachment; filename=\"notes.txt\"; filename*=UTF-8''notes.txt"
                },
                {
                  "key": "Content-Length",
                  "value": "66"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file served', () => pm.response.to.have.status(200));",
                  "pm.test('the browser will save it', () => pm.expect(pm.response.headers.get('Content-Disposition')).to.include('attachment'));"
                ]
              }
            }
          ]
        },
        {
          "name": "Download with a token · /p",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/p/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "p",
                "{{fileId}}"
              ]
            },
            "description": "Serves **any** file of the bucket — public or private — with a token; the address from\n`privateUrl`. A `BASIC` token is enough: such a token can ship inside a client app — it cannot\nlist files, only fetch those whose `id` it knows. The answer is cached by the browser only\n(`private`), and by default with `max-age=0`: the browser asks every time, and `ETag` keeps that cheap.\n\n**Token:** `BASIC`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n| `download` | query |  | `1` — download (`Content-Disposition: attachment`) instead of opening. |\n| `Range` | header |  | A byte range, e.g. `bytes=0-1023` — answers `206`; outside the file — `416`. Ignored on a temporary link with a download limit. |\n| `If-None-Match` | header |  | An `ETag` received before: when the file has not changed — `304` with no body. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file content. Headers: `Content-Type`, `Content-Length`, `ETag`, `Cache-Control`, `Content-Disposition`, `Accept-Ranges`. |\n| `206` | The requested byte range; `Content-Range` header. |\n| `304` | The file has not changed since that `ETag` — no body. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |\n| `416` | The `Range` is outside the file. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/p/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "p",
                    "{{fileId}}"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "private, max-age=0, must-revalidate"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Disposition",
                  "value": "inline; filename=\"notes.txt\"; filename*=UTF-8''notes.txt"
                },
                {
                  "key": "Content-Length",
                  "value": "66"
                }
              ],
              "body": ""
            },
            {
              "name": "401 — no token",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/p/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "p",
                    "{{fileId}}"
                  ]
                }
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 401,\n  \"message\": \"Access token required\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('served with the token', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "5. Files",
      "description": "List, search, rename and move. Deleting is in \"8. Cleanup\".",
      "item": [
        {
          "name": "List files",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files?page=1&limit=25&sort=created&dir=desc",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                },
                {
                  "key": "sort",
                  "value": "created"
                },
                {
                  "key": "dir",
                  "value": "desc"
                }
              ]
            },
            "description": "Returns the files of the bucket page by page, with search and sorting.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `page` | query |  | Page number, from 1. |\n| `limit` | query |  | Records per page. |\n| `search` | query |  | Search by name, case-insensitive. |\n| `folder` | query |  | Folder name. An empty value (`folder=`) — the root only. Without it — all files. |\n| `sort` | query |  | Sort field. |\n| `dir` | query |  | Direction. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | A page of files. |\n| `401` | No token, or the token is unknown. |\n| `403` | The token is disabled, expired, or its level is too low. |\n| `422` | The folder from the `folder` parameter does not exist. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files?page=1&limit=25&sort=created&dir=desc",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files"
                  ],
                  "query": [
                    {
                      "key": "page",
                      "value": "1"
                    },
                    {
                      "key": "limit",
                      "value": "25"
                    },
                    {
                      "key": "sort",
                      "value": "created"
                    },
                    {
                      "key": "dir",
                      "value": "desc"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "613"
                }
              ],
              "body": "{\n  \"meta\": {\n    \"current\": 1,\n    \"size\": 25,\n    \"total\": 4,\n    \"pages\": 1,\n    \"previous\": null,\n    \"next\": null\n  },\n  \"data\": [\n    {\n      \"id\": \"cZtMNnijaQ3Cy1OF\",\n      \"name\": \"notes-copy.txt\",\n      \"folder\": null,\n      \"content\": {\n        \"size\": 66,\n        \"mime\": \"text/plain\",\n        \"extension\": \"txt\",\n        \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n      },\n      \"public\": true,\n      \"deduplicated\": false,\n      \"processed\": null,\n      \"expiresAt\": null,\n      \"privateUrl\": \"https://s5.example.com/p/cZtMNnijaQ3Cy1OF\",\n      \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/cZtMNnijaQ3Cy1OF\",\n      \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n      \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n    },\n    {\n      \"id\": \"tT3q5_AMMrYnVEPp\",\n      \"name\": \"notes.txt\",\n      \"folder\": null,\n      \"content\": {\n        \"size\": 66,\n        \"mime\": \"text/plain\",\n        \"extension\": \"txt\",\n        \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n      },\n      \"public\": true,\n      \"deduplicated\": false,\n      \"processed\": null,\n      \"expiresAt\": null,\n      \"privateUrl\": \"https://s5.example.com/p/tT3q5_AMMrYnVEPp\",\n      \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/tT3q5_AMMrYnVEPp\",\n      \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n      \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n    },\n    {\n      \"id\": \"0FwMWr8RLbjRUCfD\",\n      \"name\": \"photo.webp\",\n      \"folder\": null,\n      \"content\": {\n        \"size\": 2250,\n        \"mime\": \"image/webp\",\n        \"extension\": \"webp\",\n        \"hash\": \"250c33b15669b8b0ceda27d301f46414e4e399ec9f85d9b5f9d34eeddb104381\"\n      },\n      \"public\": true,\n      \"deduplicated\": false,\n      \"processed\": null,\n      \"expiresAt\": null,\n      \"privateUrl\": \"https://s5.example.com/p/0FwMWr8RLbjRUCfD\",\n      \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/0FwMWr8RLbjRUCfD\",\n      \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n      \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n    },\n    {\n      \"id\": \"60I4PpbPsXVagANk\",\n      \"name\": \"photo.png\",\n      \"folder\": null,\n      \"content\": {\n        \"size\": 4654,\n        \"mime\": \"image/png\",\n        \"extension\": \"png\",\n        \"hash\": \"ee300dd3885906657a97edefa78057610a91570d6e594a764fab92c6db518572\"\n      },\n      \"public\": true,\n      \"deduplicated\": false,\n      \"processed\": null,\n      \"expiresAt\": null,\n      \"privateUrl\": \"https://s5.example.com/p/60I4PpbPsXVagANk\",\n      \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/60I4PpbPsXVagANk\",\n      \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n      \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n    }\n  ]\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('list received', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Get a file",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}"
              ]
            },
            "description": "Returns one file — name, folder, size, type, hash and addresses.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "489"
                }
              ],
              "body": "{\n  \"id\": \"tT3q5_AMMrYnVEPp\",\n  \"name\": \"notes.txt\",\n  \"folder\": null,\n  \"content\": {\n    \"size\": 66,\n    \"mime\": \"text/plain\",\n    \"extension\": \"txt\",\n    \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n  },\n  \"public\": true,\n  \"deduplicated\": false,\n  \"processed\": null,\n  \"expiresAt\": null,\n  \"privateUrl\": \"https://s5.example.com/p/tT3q5_AMMrYnVEPp\",\n  \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/tT3q5_AMMrYnVEPp\",\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "404 — no such file",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"File does not exist\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file found', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Rename a file",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}"
              ]
            },
            "description": "Changes the name of the file and the folder it is in. The content and the `id` stay.\n\n`folder` says **where the file will be after the request**. To only rename a file inside a\nfolder, pass that folder's name — otherwise the file moves to the root.\n\nA moved file takes the rules of its new folder — visibility and retention; retention counts from\nthe move. **Moving to the root makes the file public**: it opens at `/o/…` without a token.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | yes | New name. `/`, `\\` and control characters become `-`; the extension is not adjusted. |\n| `folder` | string or null |  | The folder the file should end up in. Without it — the root. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file with its new `name`, `folder`, `public` and `expiresAt`. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |\n| `409` | The name is already taken in that folder. |\n| `422` | No such folder, or the name is empty once cleaned. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"notes-renamed.txt\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes-renamed.txt\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "497"
                }
              ],
              "body": "{\n  \"id\": \"tT3q5_AMMrYnVEPp\",\n  \"name\": \"notes-renamed.txt\",\n  \"folder\": null,\n  \"content\": {\n    \"size\": 66,\n    \"mime\": \"text/plain\",\n    \"extension\": \"txt\",\n    \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n  },\n  \"public\": true,\n  \"deduplicated\": false,\n  \"processed\": null,\n  \"expiresAt\": null,\n  \"privateUrl\": \"https://s5.example.com/p/tT3q5_AMMrYnVEPp\",\n  \"publicUrl\": \"https://s5.example.com/o/2faa480a-4f40-4131-b2ce-5cd6ced969a8/tT3q5_AMMrYnVEPp\",\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            },
            {
              "name": "409 — name taken",
              "originalRequest": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes-renamed.txt\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Conflict",
              "code": 409,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 409,\n  \"message\": \"A file named \\\"taken.txt\\\" already exists here\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file renamed', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Move a file into a folder",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}"
              ]
            },
            "description": "Changes the name of the file and the folder it is in. The content and the `id` stay.\n\n`folder` says **where the file will be after the request**. To only rename a file inside a\nfolder, pass that folder's name — otherwise the file moves to the root.\n\nA moved file takes the rules of its new folder — visibility and retention; retention counts from\nthe move. **Moving to the root makes the file public**: it opens at `/o/…` without a token.\n\nHere the file moves into a private folder: afterwards `publicUrl` is `null` and `/o/…` stops working.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `name` | string | yes | New name. `/`, `\\` and control characters become `-`; the extension is not adjusted. |\n| `folder` | string or null |  | The folder the file should end up in. Without it — the root. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file with its new `name`, `folder`, `public` and `expiresAt`. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |\n| `409` | The name is already taken in that folder. |\n| `422` | No such folder, or the name is empty once cleaned. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"notes-renamed.txt\",\n  \"folder\": \"{{folderName}}\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "PUT",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"notes-renamed.txt\",\n  \"folder\": \"{{folderName}}\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "462"
                }
              ],
              "body": "{\n  \"id\": \"tT3q5_AMMrYnVEPp\",\n  \"name\": \"notes-renamed.txt\",\n  \"folder\": \"demo-1794090657532\",\n  \"content\": {\n    \"size\": 66,\n    \"mime\": \"text/plain\",\n    \"extension\": \"txt\",\n    \"hash\": \"2efdc909f33e07505ad104602c9eb3db5de1b54f0562a5c524020c194930599a\"\n  },\n  \"public\": false,\n  \"deduplicated\": false,\n  \"processed\": null,\n  \"expiresAt\": \"2026-11-14T22:30:57+00:00\",\n  \"privateUrl\": \"https://s5.example.com/p/tT3q5_AMMrYnVEPp\",\n  \"publicUrl\": null,\n  \"createdAt\": \"2026-11-07T22:30:57+00:00\",\n  \"updatedAt\": \"2026-11-07T22:30:57+00:00\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file moved', () => pm.response.to.have.status(200));",
                  "const json = pm.response.json();",
                  "pm.test('now private', () => pm.expect(json.public).to.eql(false));",
                  "pm.test('retention applied', () => pm.expect(json.expiresAt).to.be.a('string'));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "6. Temporary links",
      "description": "A `/t/…` link to one file: opens without a token, but only until it expires and, if needed,\na limited number of times. Works for private files too.",
      "item": [
        {
          "name": "Issue a temporary link",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}/link",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}",
                "link"
              ]
            },
            "description": "Issues a signed `/t/…` link to the file. It opens without a token, but only until it expires.\n\n**Two kinds.** Without `maxDownloads` and `revocable` the link is **signed**: it is not stored,\n`id: null`, cannot be revoked on its own — only all links of the bucket at once — and does not\nkeep a `note`. With either field it is **tracked**: stored, revocable on its own, listed. It counts\ndownloads only with `maxDownloads` — with `revocable` alone `downloads` stays `0`. A link with\n`maxDownloads` does not support `Range`: the file is served whole only.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `ttl` | integer |  | Lifetime, seconds: up to 30 days. Without the field — 3600. |\n| `disposition` | `0` · `1` |  | Open in the browser (if the type allows it) or download. (`0` open in the browser if the type allows it (as on `/o`), `1` download) |\n| `maxDownloads` | integer |  | Download limit. Makes the link tracked. |\n| `revocable` | boolean |  | Revocable on its own. Makes the link tracked. |\n| `note` | string |  | A note for yourself; kept only on a tracked link. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The link is issued. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |\n| `422` | Validation failed — for example, `ttl` over 30 days. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ttl\": 3600,\n  \"disposition\": 0\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}/link",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}",
                    "link"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ttl\": 3600,\n  \"disposition\": 0\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "247"
                }
              ],
              "body": "{\n  \"id\": null,\n  \"url\": \"https://s5.example.com/t/AQAAAAAAAAALau-0sQAAAAADGcZfqEMAcFy4RevV_H5Vlw\",\n  \"expiresAt\": \"2026-11-07T23:30:57+00:00\",\n  \"revocable\": false,\n  \"maxDownloads\": null,\n  \"downloads\": 0,\n  \"revoked\": false,\n  \"disposition\": {\n    \"id\": 0,\n    \"name\": \"INLINE\"\n  },\n  \"note\": \"\"\n}"
            },
            {
              "name": "422 — ttl over 30 days",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}/link",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}",
                    "link"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ttl\": 3600,\n  \"disposition\": 0\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Validation failed\",\n  \"errors\": {\n    \"ttl\": [\n      \"must not exceed 2592000\"\n    ]\n  }\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('link issued', () => pm.response.to.have.status(201));",
                  "const json = pm.response.json();",
                  "pm.collectionVariables.set('linkUrl', json.url);",
                  "pm.test('signed link — no id', () => pm.expect(json.id).to.eql(null));"
                ]
              }
            }
          ]
        },
        {
          "name": "Issue a link with a download limit",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}/link",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}",
                "link"
              ]
            },
            "description": "Issues a signed `/t/…` link to the file. It opens without a token, but only until it expires.\n\n**Two kinds.** Without `maxDownloads` and `revocable` the link is **signed**: it is not stored,\n`id: null`, cannot be revoked on its own — only all links of the bucket at once — and does not\nkeep a `note`. With either field it is **tracked**: stored, revocable on its own, listed. It counts\ndownloads only with `maxDownloads` — with `revocable` alone `downloads` stays `0`. A link with\n`maxDownloads` does not support `Range`: the file is served whole only.\n\nA tracked link for a day and 3 downloads, revocable. After the third download it answers `404`.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Request body**\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `ttl` | integer |  | Lifetime, seconds: up to 30 days. Without the field — 3600. |\n| `disposition` | `0` · `1` |  | Open in the browser (if the type allows it) or download. (`0` open in the browser if the type allows it (as on `/o`), `1` download) |\n| `maxDownloads` | integer |  | Download limit. Makes the link tracked. |\n| `revocable` | boolean |  | Revocable on its own. Makes the link tracked. |\n| `note` | string |  | A note for yourself; kept only on a tracked link. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `201` | The link is issued. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |\n| `422` | Validation failed — for example, `ttl` over 30 days. |",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"ttl\": 86400,\n  \"disposition\": 1,\n  \"maxDownloads\": 3,\n  \"revocable\": true,\n  \"note\": \"for the contractor\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "201 — success",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}/link",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}",
                    "link"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"ttl\": 86400,\n  \"disposition\": 1,\n  \"maxDownloads\": 3,\n  \"revocable\": true,\n  \"note\": \"for the contractor\"\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Created",
              "code": 201,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "272"
                }
              ],
              "body": "{\n  \"id\": 3,\n  \"url\": \"https://s5.example.com/t/AQAAAAAAAAALavD4IQMAAAADAAAAAAAAAANNMm_jgUcKZtGosvmbCaJr\",\n  \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n  \"revocable\": true,\n  \"maxDownloads\": 3,\n  \"downloads\": 0,\n  \"revoked\": false,\n  \"disposition\": {\n    \"id\": 1,\n    \"name\": \"ATTACHMENT\"\n  },\n  \"note\": \"for the contractor\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('link issued', () => pm.response.to.have.status(201));",
                  "pm.collectionVariables.set('linkId', pm.response.json().id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Open a temporary link · /t",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{linkUrl}}",
              "host": [
                "{{linkUrl}}"
              ]
            },
            "description": "Serves the file through a temporary link, without a token. After expiry, revocation or the download limit — `404`. It downloads when issued with `disposition: 1` or when the type may not be shown in a browser (the same rules as `/o`). A link with `maxDownloads` does not support `Range`: the file is served whole, `200`.\n\n**Token:** not needed\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `token` | path | yes | The signed part of the link after `/t/`. |\n| `Range` | header |  | A byte range, e.g. `bytes=0-1023` — answers `206`; outside the file — `416`. Ignored on a temporary link with a download limit. |\n| `If-None-Match` | header |  | An `ETag` received before: when the file has not changed — `304` with no body. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | The file content. Headers: `Content-Type`, `Content-Length`, `ETag`, `Cache-Control`, `Content-Disposition`, `Accept-Ranges`. |\n| `206` | The requested byte range; `Content-Range` header. |\n| `304` | The file has not changed since that `ETag` — no body. |\n| `404` | The link expired, was revoked or is used up. |\n| `416` | The `Range` is outside the file. |",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{linkUrl}}",
                  "host": [
                    "{{linkUrl}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Etag",
                  "value": "\"6aefa6a1-42\""
                },
                {
                  "key": "Accept-Ranges",
                  "value": "bytes"
                },
                {
                  "key": "Cache-Control",
                  "value": "private, max-age=3600"
                },
                {
                  "key": "Content-Type",
                  "value": "text/plain"
                },
                {
                  "key": "Content-Disposition",
                  "value": "inline; filename=\"notes-renamed.txt\"; filename*=UTF-8''notes-renamed.txt"
                },
                {
                  "key": "Content-Length",
                  "value": "66"
                }
              ],
              "body": ""
            },
            {
              "name": "404 — link expired or revoked",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{linkUrl}}",
                  "host": [
                    "{{linkUrl}}"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"Link is invalid or has expired\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('served through the link', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Links of a file",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}/links",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}",
                "links"
              ]
            },
            "description": "The last 20 tracked links of the file, newest first — with download counts, expiry and the revoked flag; expired, revoked and used-up ones too: a link is kept until 7 days after its `expiresAt`. Signed links are not here — they are not stored anywhere.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | An array of links. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}/links",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}",
                    "links"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "274"
                }
              ],
              "body": "[\n  {\n    \"id\": 3,\n    \"url\": \"https://s5.example.com/t/AQAAAAAAAAALavD4IQMAAAADAAAAAAAAAANNMm_jgUcKZtGosvmbCaJr\",\n    \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n    \"revocable\": true,\n    \"maxDownloads\": 3,\n    \"downloads\": 0,\n    \"revoked\": false,\n    \"disposition\": {\n      \"id\": 1,\n      \"name\": \"ATTACHMENT\"\n    },\n    \"note\": \"for the contractor\"\n  }\n]"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('list received', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "All links of the bucket",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/links?page=1&limit=25",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "links"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "limit",
                  "value": "25"
                }
              ]
            },
            "description": "Tracked links of the whole bucket page by page, newest first — expired, revoked and used-up ones included: a link is kept until 7 days after its `expiresAt`.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `page` | query |  | Page number, from 1. |\n| `limit` | query |  | Records per page. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | A page of links. |\n| `401` | No token, or the token is unknown. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/links?page=1&limit=25",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "links"
                  ],
                  "query": [
                    {
                      "key": "page",
                      "value": "1"
                    },
                    {
                      "key": "limit",
                      "value": "25"
                    }
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "362"
                }
              ],
              "body": "{\n  \"meta\": {\n    \"current\": 1,\n    \"size\": 25,\n    \"total\": 1,\n    \"pages\": 1,\n    \"previous\": null,\n    \"next\": null\n  },\n  \"data\": [\n    {\n      \"id\": 3,\n      \"url\": \"https://s5.example.com/t/AQAAAAAAAAALavD4IQMAAAADAAAAAAAAAANNMm_jgUcKZtGosvmbCaJr\",\n      \"expiresAt\": \"2026-11-08T22:30:57+00:00\",\n      \"revocable\": true,\n      \"maxDownloads\": 3,\n      \"downloads\": 0,\n      \"revoked\": false,\n      \"disposition\": {\n        \"id\": 1,\n        \"name\": \"ATTACHMENT\"\n      },\n      \"note\": \"for the contractor\"\n    }\n  ]\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('list received', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "7. Error examples",
      "description": "Requests that fail on purpose. Every error looks the same: `{ \"code\", \"message\" }`, plus `errors` when validation failed.",
      "item": [
        {
          "name": "No token → 401",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/check",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "check"
              ]
            },
            "description": "A request without `Authorization`. An unknown token gets the same.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [
            {
              "name": "401 — no token",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/check",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "check"
                  ]
                },
                "auth": {
                  "type": "noauth"
                }
              },
              "status": "Unauthorized",
              "code": 401,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 401,\n  \"message\": \"Access token required\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('401', () => pm.response.to.have.status(401));"
                ]
              }
            }
          ]
        },
        {
          "name": "No such file → 404",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/AAAAAAAAAAAAAAAA",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "AAAAAAAAAAAAAAAA"
              ]
            },
            "description": "No file with this `id` in the bucket of the token. Someone else's file looks the same — the answer does not reveal other buckets."
          },
          "response": [
            {
              "name": "404 — no such file",
              "originalRequest": {
                "method": "GET",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/AAAAAAAAAAAAAAAA",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "AAAAAAAAAAAAAAAA"
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"File does not exist\"\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ]
        },
        {
          "name": "Invalid parameters → 422",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/v1/folders",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders"
              ]
            },
            "description": "Validation failed. `errors` has a message per field: show them next to the input.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"name\": \"/bad name\",\n  \"retention\": 99\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "response": [
            {
              "name": "422 — validation failed",
              "originalRequest": {
                "method": "POST",
                "header": [
                  {
                    "key": "Content-Type",
                    "value": "application/json"
                  }
                ],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders"
                  ]
                },
                "body": {
                  "mode": "raw",
                  "raw": "{\n  \"name\": \"/bad name\",\n  \"retention\": 99\n}",
                  "options": {
                    "raw": {
                      "language": "json"
                    }
                  }
                }
              },
              "status": "Unprocessable Entity",
              "code": 422,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 422,\n  \"message\": \"Validation failed\",\n  \"errors\": {\n    \"name\": [\n      \"may contain letters, digits, space, dot, underscore, dash\"\n    ],\n    \"retention\": [\n      \"retention must be one of [0, 1, 2, 3, 4, 5, 6], got '99'\"\n    ]\n  }\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('422', () => pm.response.to.have.status(422));",
                  "pm.test('per-field errors', () => pm.expect(pm.response.json().errors).to.have.property('name'));"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "8. Cleanup",
      "description": "Deletes everything the run created, so it can be repeated. Requests with nothing to delete skip themselves.",
      "item": [
        {
          "name": "Revoke a link",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/links/{{linkId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "links",
                "{{linkId}}"
              ]
            },
            "description": "Revokes one tracked link. It stops opening at once.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | `id` of a tracked link. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `204` | Revoked, no body. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such link. |"
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/links/{{linkId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "links",
                    "{{linkId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            },
            {
              "name": "404 — no such link",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/links/{{linkId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "links",
                    "{{linkId}}"
                  ]
                }
              },
              "status": "Not Found",
              "code": 404,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json"
                }
              ],
              "body": "{\n  \"code\": 404,\n  \"message\": \"Share link does not exist\"\n}"
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('linkId')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('link revoked', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Revoke every link of the bucket",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/links",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "links"
              ]
            },
            "description": "Revokes **every** temporary link of the bucket, signed ones included. New links are issued as usual afterwards. For a leaked link that cannot be revoked on its own.\n\n**Token:** `FULL`\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `200` | Number of the new signing epoch. |\n| `401` | No token, or the token is unknown. |"
          },
          "response": [
            {
              "name": "200 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/links",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "links"
                  ]
                }
              },
              "status": "OK",
              "code": 200,
              "_postman_previewlanguage": "json",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "application/json; charset=utf-8"
                },
                {
                  "key": "Content-Length",
                  "value": "11"
                }
              ],
              "body": "{\n  \"epoch\": 4\n}"
            }
          ],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('links revoked', () => pm.response.to.have.status(200));"
                ]
              }
            }
          ]
        },
        {
          "name": "Check: the link no longer works",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{linkUrl}}",
              "host": [
                "{{linkUrl}}"
              ]
            },
            "description": "The same link that opened in section 6 — after revoking every link it answers `404`.",
            "auth": {
              "type": "noauth"
            }
          },
          "response": [],
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('link is dead — 404', () => pm.response.to.have.status(404));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete a file",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{fileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{fileId}}"
              ]
            },
            "description": "Deletes the file. Its addresses and temporary links stop working.\n\nWhen another file of the bucket has the same content, the content stays on disk; space is freed\nwhen the last file with that content is deleted.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `id` | path | yes | File id from the `id` field, 16 characters. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `204` | Deleted, no body. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such file in the bucket of the token. |"
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{fileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{fileId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('fileId')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file deleted', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the copy",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{dedupFileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{dedupFileId}}"
              ]
            },
            "description": "Deletes the file from \"Skip the transfer\"."
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{dedupFileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{dedupFileId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('dedupFileId')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file deleted', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the uploaded file",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{uploadedFileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{uploadedFileId}}"
              ]
            },
            "description": "Deletes the file from \"Upload a file\", if it was uploaded."
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{uploadedFileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{uploadedFileId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('uploadedFileId')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('file deleted', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the uploaded image",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/files/{{imageFileId}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "files",
                "{{imageFileId}}"
              ]
            },
            "description": "Deletes the image from \"Upload an image with processing\", if it was uploaded."
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/files/{{imageFileId}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "files",
                    "{{imageFileId}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('imageFileId')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('image deleted', () => pm.response.to.have.status(204));"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete the folder",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
              "host": [
                "{{baseUrl}}"
              ],
              "path": [
                "v1",
                "folders",
                "{{folderName}}"
              ]
            },
            "description": "Deletes the folder **together with every file in it**. Cannot be undone.\n\n**Token:** `FULL`\n\n**Parameters**\n\n| Name | In | Required | Description |\n|---|---|---|---|\n| `name` | path | yes | Folder name. |\n\n**Responses**\n\n| Code | When |\n|---|---|\n| `204` | Deleted, no body. |\n| `401` | No token, or the token is unknown. |\n| `404` | No such folder. |"
          },
          "response": [
            {
              "name": "204 — success",
              "originalRequest": {
                "method": "DELETE",
                "header": [],
                "url": {
                  "raw": "{{baseUrl}}/v1/folders/{{folderName}}",
                  "host": [
                    "{{baseUrl}}"
                  ],
                  "path": [
                    "v1",
                    "folders",
                    "{{folderName}}"
                  ]
                }
              },
              "status": "No Content",
              "code": 204,
              "_postman_previewlanguage": "text",
              "header": [
                {
                  "key": "Content-Type",
                  "value": "text/html"
                },
                {
                  "key": "Content-Length",
                  "value": "0"
                }
              ],
              "body": ""
            }
          ],
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('folderName')) {",
                  "    // The earlier step did not run — skip this one, the run goes on.",
                  "    pm.execution.skipRequest();",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('folder deleted', () => pm.response.to.have.status(204));",
                  "['fileId', 'dedupFileId', 'uploadedFileId', 'imageFileId', 'linkId', 'uploadId', 'cancelUploadId'].forEach((k) => pm.collectionVariables.set(k, ''));"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "http://localhost:9090",
      "description": "API address with the scheme, no trailing /"
    },
    {
      "key": "token",
      "value": "",
      "description": "A full-access bucket token — into Current value"
    },
    {
      "key": "bucketId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "folderName",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "uploadedFileId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "imageFileId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "chunkText",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "chunkSize",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "contentHash",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "uploadId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "uploadOffset",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "fileId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "dedupFileId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "cancelUploadId",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "etag",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "linkUrl",
      "value": "",
      "description": "filled in during the run"
    },
    {
      "key": "linkId",
      "value": "",
      "description": "filled in during the run"
    }
  ]
}