{ "openapi": "3.1.0", "info": { "title": "ProjectSend", "version": "1.0.0", "description": "Staff-authenticated REST API for ProjectSend. Authenticate with a bearer token created under Settings \u2192 API tokens. See docs/api-guide.md for the narrative guide." }, "servers": [ { "url": "/api/v1" } ], "security": [ { "http": [] } ], "paths": { "/activity": { "get": { "operationId": "activity.index", "description": "Filter by `action` \u2014 repeat the parameter for more than one, as\n`?action[]=file.assigned&action[]=file.downloaded`. `subject_type`\nnarrows to one kind of thing (`file`, `user`, `group`, \u2026).\n\nEntries are never edited, so `updated_since` walks the moment each\none was recorded. Everything else about polling is the shape every\nlist endpoint here shares.\n\nScoped to what the caller may read: a staff member limited to their\nassigned clients sees entries about their own library and their own\nactions, never the whole installation's.\n\nRequires a token with the ability: `view_actions_log`.", "summary": "List activity, newest first", "tags": [ "Activity" ], "parameters": [ { "name": "updated_since", "in": "query", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "per_page", "in": "query", "schema": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": [ "string", "null" ] } }, { "name": "action[]", "in": "query", "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Action" } } }, { "name": "subject_type", "in": "query", "schema": { "type": [ "string", "null" ], "maxLength": 64 } } ], "responses": { "200": { "description": "created_at, not updated_at: the log is appended to and never\nedited, and has no updated_at column to walk.\n\n\n\nPaginated set of `ActivityResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/ActivityResource" } }, "links": { "type": "object", "properties": { "first": { "type": [ "string", "null" ] }, "last": { "type": [ "string", "null" ] }, "prev": { "type": [ "string", "null" ] }, "next": { "type": [ "string", "null" ] } }, "required": [ "first", "last", "prev", "next" ] }, "meta": { "type": "object", "properties": { "path": { "type": [ "string", "null" ], "description": "Base path for paginator generated URLs." }, "per_page": { "type": "integer", "description": "Number of items shown per page.", "minimum": 0 }, "next_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the next set of items." }, "prev_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the previous set of items." } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "links", "meta" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads": { "post": { "operationId": "uploads.store", "description": "Declare the filename and size; the response returns an `uploadId`\nused by the remaining steps. The declared size is re-checked against\nthe assembled bytes when the upload completes.\n\nRequires a token with the ability: `upload`.", "summary": "Begin a resumable upload", "tags": [ "ChunkedUploads" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "filename": { "type": "string", "maxLength": 255 }, "size": { "type": "integer", "minimum": 1 }, "type": { "type": [ "string", "null" ], "maxLength": 255 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "folder_id": { "type": [ "integer", "null" ] }, "previous_file_id": { "type": [ "integer", "null" ] } }, "required": [ "filename", "size" ] } } } }, "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "uploadId": { "type": "string" }, "key": { "type": "string" } }, "required": [ "uploadId", "key" ] } } } }, "403": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads/{session}/parts/{part}/sign": { "get": { "operationId": "uploads.parts.sign", "description": "Requires a token with the ability: `upload`.", "summary": "Get a short-lived signed URL for one part", "tags": [ "ChunkedUploads" ], "parameters": [ { "name": "session", "in": "path", "required": true, "description": "The session ID", "schema": { "type": "string", "format": "uuid" } }, { "name": "part", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "url": { "type": "string" }, "method": { "type": "string", "const": "PUT" } }, "required": [ "url", "method" ] } } } }, "422": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads/{session}/parts/{part}": { "put": { "operationId": "uploads.parts.put", "description": "PUT the part's raw bytes to the signed URL returned by the sign\nendpoint. Parts may be sent in any order, and the response's `ETag`\nidentifies the stored part.\n\nRequires a token with the ability: `upload`.", "summary": "Upload one part", "tags": [ "ChunkedUploads" ], "parameters": [ { "name": "session", "in": "path", "required": true, "description": "The session ID", "schema": { "type": "string", "format": "uuid" } }, { "name": "part", "in": "path", "required": true, "schema": { "type": "integer" } } ], "responses": { "200": { "description": "", "headers": { "ETag": { "schema": { "type": "string" } } } }, "422": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "413": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads/{session}/parts": { "get": { "operationId": "uploads.parts.index", "description": "Requires a token with the ability: `upload`.", "summary": "List the parts already received, so an interrupted upload can resume\nrather than start again", "tags": [ "ChunkedUploads" ], "parameters": [ { "name": "session", "in": "path", "required": true, "description": "The session ID", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads/{session}/complete": { "post": { "operationId": "uploads.complete", "description": "Requires a token with the ability: `upload`.", "tags": [ "ChunkedUploads" ], "parameters": [ { "name": "session", "in": "path", "required": true, "description": "The session ID", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "location": { "type": "string" }, "file_id": { "type": "integer" }, "version_error": { "type": [ "string", "null" ] } }, "required": [ "location", "file_id", "version_error" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/uploads/{session}": { "delete": { "operationId": "uploads.destroy", "description": "Requires a token with the ability: `upload`.", "tags": [ "ChunkedUploads" ], "parameters": [ { "name": "session", "in": "path", "required": true, "description": "The session ID", "schema": { "type": "string", "format": "uuid" } } ], "responses": { "204": { "description": "No content" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/clients": { "get": { "operationId": "clients.index", "description": "Requires a token with the ability: `manage_clients`.", "tags": [ "Clients" ], "parameters": [ { "name": "updated_since", "in": "query", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "per_page", "in": "query", "schema": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": [ "string", "null" ] } }, { "name": "search", "in": "query", "schema": { "type": [ "string", "null" ], "maxLength": 255 } }, { "name": "status", "in": "query", "schema": { "type": [ "string", "null" ], "enum": [ "active", "inactive", null ] } } ], "responses": { "200": { "description": "Paginated set of `ClientResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/ClientResource" } }, "links": { "type": "object", "properties": { "first": { "type": [ "string", "null" ] }, "last": { "type": [ "string", "null" ] }, "prev": { "type": [ "string", "null" ] }, "next": { "type": [ "string", "null" ] } }, "required": [ "first", "last", "prev", "next" ] }, "meta": { "type": "object", "properties": { "path": { "type": [ "string", "null" ], "description": "Base path for paginator generated URLs." }, "per_page": { "type": "integer", "description": "Number of items shown per page.", "minimum": 0 }, "next_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the next set of items." }, "prev_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the previous set of items." } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "links", "meta" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "post": { "operationId": "clients.store", "description": "Requires a token with the ability: `create_clients`.", "tags": [ "Clients" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "email": { "type": "string", "format": "email", "maxLength": 255 }, "password": { "type": "string", "description": "No `confirmed`: repeating a password is a defence against a\nhuman mistyping into a form, and an API caller has no second\nfield to mistype. This installation's password policy still\napplies \u2014 a minimum length, and optionally a check against\nknown breaches. Both are configured under Settings \u2192\nSecurity, so read them from there rather than assuming the\ndefaults; a password this endpoint accepts on one\ninstallation may be refused on another." }, "storage_quota_mb": { "type": [ "integer", "null" ], "minimum": 0 }, "custom_field_values": { "type": "array", "items": { "type": "string" } } }, "required": [ "name", "email", "password" ] } } } }, "responses": { "201": { "description": "`ClientResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ClientResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/clients/{client}": { "get": { "operationId": "clients.show", "description": "Requires a token with the ability: `edit_clients`.", "tags": [ "Clients" ], "parameters": [ { "name": "client", "in": "path", "required": true, "description": "The client ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`ClientResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ClientResource" } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "patch": { "operationId": "clients.update", "description": "Requires a token with the ability: `edit_clients`.", "tags": [ "Clients" ], "parameters": [ { "name": "client", "in": "path", "required": true, "description": "The client ID", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "email": { "type": "string", "format": "email", "maxLength": 255 }, "active": { "type": "boolean" }, "password": { "type": [ "string", "null" ] }, "storage_quota_mb": { "type": [ "integer", "null" ], "minimum": 0 }, "custom_field_values": { "type": "array", "items": { "type": "string" } } } } } } }, "responses": { "200": { "description": "`ClientResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ClientResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "clients.destroy", "description": "If the client owns no files or folders, no body is needed.\n\nIf they do, you must say what happens to that content: send\n`content_action` as either `cascade_delete` (delete it along with the\naccount) or `reassign`, and in the latter case a `reassign_to_id`\nnaming the active account that inherits it. Omitting the choice is a\n422 \u2014 there is no default, because one would silently destroy a\nclient's files and the other would silently hand them to somebody\nelse.\n\n`GET /clients/{client}` reports the counts so you can decide before\ncalling this.\n\nRequires a token with the ability: `delete_clients`.", "summary": "Delete a client", "tags": [ "Clients" ], "parameters": [ { "name": "client", "in": "path", "required": true, "description": "The client ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/clients/{client}/two-factor": { "delete": { "operationId": "clients.two-factor.destroy", "description": "The remedy for a locked-out account: a client whose authenticator\napp and recovery codes are both gone cannot sign in, and nobody else\ncan open the account for them either. Afterwards they sign in with\ntheir password alone, and \u2014 if this installation enforces two-factor\nauthentication for clients \u2014 are asked to enrol again on their next\nrequest.\n\nThe client is emailed that this happened, and the action is recorded\nin the activity log against the caller. Answers 204 whether or not a\nsecond factor was actually in force.\n\nRequires a token with the ability: `edit_clients`.", "summary": "Remove a client's two-factor authentication", "tags": [ "Clients" ], "parameters": [ { "name": "client", "in": "path", "required": true, "description": "The client ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/comments/pending": { "get": { "operationId": "comments.pending", "description": "Scoped by the same file boundary as everything else \u2014 the whole of\nit, not just its library half: a client-scoped token sees pending\ncomments only on files its owner could already open, and a token\nwhose owner holds no file key at all sees none. Oldest first, so\nworking through the list means working through the backlog.\n\nRequires a token with the ability: `moderate_comments`.", "summary": "List comments awaiting approval", "tags": [ "CommentModeration" ], "responses": { "200": { "description": "Array of `FileCommentResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/FileCommentResource" } } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/comments/{comment}/approve": { "post": { "operationId": "comments.approve", "description": "Nobody can see it until this happens. Approving an already-approved\ncomment changes nothing and announces nothing, so a retried request\nis safe \u2014 which matters more here than on the web, where a human does\nnot retry automatically.\n\nRequires a token with the ability: `moderate_comments`.", "summary": "Approve a comment left by a visitor", "tags": [ "CommentModeration" ], "parameters": [ { "name": "comment", "in": "path", "required": true, "description": "The comment ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`FileCommentResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileCommentResource" } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/tokens/current": { "delete": { "operationId": "tokens.current.destroy", "tags": [ "CurrentToken" ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files/{file}/assignments": { "post": { "operationId": "files.assignments.store", "description": "Requires a token with any of these abilities: `edit_files`, `edit_others_files`.", "tags": [ "FileAssignments" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/FileResource" }, { "type": "object", "required": [ "assignments" ] } ] } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "files.assignments.destroy", "description": "Requires a token with any of these abilities: `edit_files`, `edit_others_files`.", "tags": [ "FileAssignments" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/FileResource" }, { "type": "object", "required": [ "assignments" ] } ] } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files/{file}/comments": { "get": { "operationId": "files.comments.index", "description": "Returns the comments the authenticated token's owner may read,\noldest first. A comment left by a visitor and not yet approved is\nincluded only for a token whose owner may moderate.\n\nRequires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "summary": "List a file's comments", "tags": [ "FileComments" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "Array of `FileCommentResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/FileCommentResource" } } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "post": { "operationId": "files.comments.store", "description": "`visibility` is one of `only_me` (a private note), `staff_only` (the\nteam, no client), `clients` (**the team and** every client the file\nis shared with \u2014 the name says who it adds, since staff can already\nsee everything on a file they can open) or `everyone` (all of the\nabove plus anyone who opens the file without logging in).\nWhich of them are available depends on the installation's comment\nsettings and on whether the file is publicly visible; asking for one\nthat is not available returns 403.\n\n`reply_to` is the id of a comment being answered. A reply inherits\nthat comment's audience, which is the only way a comment becomes\naddressed to one client rather than all of them \u2014 there is no field\nthat names a client directly. An id this token cannot already read\nis ignored, and the comment is posted as a fresh one.\n\nRequires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "summary": "Post a comment on a file", "tags": [ "FileComments" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "body": { "type": "string", "maxLength": 5000 }, "visibility": { "$ref": "#/components/schemas/CommentVisibility" }, "reply_to": { "type": [ "integer", "null" ] } }, "required": [ "body", "visibility" ] } } } }, "responses": { "201": { "description": "`FileCommentResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileCommentResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/comments/{comment}": { "patch": { "operationId": "comments.update", "description": "Only its own author may, and only within the installation's editing\nwindow. Moderation rights do not extend to rewriting somebody\nelse's words.\n\nRequires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "summary": "Edit a comment", "tags": [ "FileComments" ], "parameters": [ { "name": "comment", "in": "path", "required": true, "description": "The comment ID", "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "body": { "type": "string", "maxLength": 5000 } }, "required": [ "body" ] } } } }, "responses": { "200": { "description": "`FileCommentResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileCommentResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "comments.destroy", "description": "Its author may, within the editing window; a moderator may at any\ntime. The comment is soft-deleted, so a later dispute is not left\nwith a hole where the conversation was.\n\nRequires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "summary": "Delete a comment", "tags": [ "FileComments" ], "parameters": [ { "name": "comment", "in": "path", "required": true, "description": "The comment ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files/{file}/download": { "get": { "operationId": "files.download", "description": "Requires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "tags": [ "FileDownload" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object" } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files/{file}/version": { "put": { "operationId": "files.version.store", "description": "The file named by `previous_file_id` must be one you may reach, and\nmust not already have been revised. **A revision is shared with the\nsame people as the original**: any recipients this file currently has\nare moved onto the original, and afterwards its own assignment\nendpoints refuse writes and point at the original instead.\n\nRequires a token with any of these abilities: `edit_files`, `edit_others_files`.", "summary": "Mark this file as a new version of another", "tags": [ "FileVersions" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "previous_file_id": { "type": "integer" } }, "required": [ "previous_file_id" ] } } } }, "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileResource" } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "422": { "$ref": "#/components/responses/ValidationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "files.version.destroy", "description": "It stops inheriting the original's recipients, so it keeps a copy of\nthem: unlinking never takes access away from someone who already has\nit.\n\nRequires a token with any of these abilities: `edit_files`, `edit_others_files`.", "summary": "Remove this file's version link, making it stand on its own again", "tags": [ "FileVersions" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileResource" } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files": { "get": { "operationId": "files.index", "description": "Requires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "tags": [ "Files" ], "parameters": [ { "name": "updated_since", "in": "query", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "per_page", "in": "query", "schema": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": [ "string", "null" ] } }, { "name": "folder_id", "in": "query", "schema": { "type": [ "integer", "null" ] } }, { "name": "category_id", "in": "query", "schema": { "type": [ "integer", "null" ] } }, { "name": "uploaded_by", "in": "query", "schema": { "type": [ "integer", "null" ] } }, { "name": "search", "in": "query", "schema": { "type": [ "string", "null" ], "maxLength": 255 } }, { "name": "public", "in": "query", "schema": { "type": [ "boolean", "null" ] } }, { "name": "expired", "in": "query", "schema": { "type": [ "boolean", "null" ] } } ], "responses": { "200": { "description": "Paginated set of `FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/FileResource" } }, "links": { "type": "object", "properties": { "first": { "type": [ "string", "null" ] }, "last": { "type": [ "string", "null" ] }, "prev": { "type": [ "string", "null" ] }, "next": { "type": [ "string", "null" ] } }, "required": [ "first", "last", "prev", "next" ] }, "meta": { "type": "object", "properties": { "path": { "type": [ "string", "null" ], "description": "Base path for paginator generated URLs." }, "per_page": { "type": "integer", "description": "Number of items shown per page.", "minimum": 0 }, "next_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the next set of items." }, "prev_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the previous set of items." } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "links", "meta" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "post": { "operationId": "files.store", "description": "Send the file as multipart form data. The maximum accepted size is\nthis installation's configured upload limit; larger or unreliable\nuploads should use the resumable `/uploads` endpoints instead.\n\nThe stored content type is detected from the uploaded bytes, not from\nthe declared `Content-Type`.\n\nRequires a token with the ability: `upload`.", "summary": "Upload a file in a single request", "tags": [ "Files" ], "requestBody": { "required": true, "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary", "contentMediaType": "application/octet-stream" }, "name": { "type": [ "string", "null" ], "maxLength": 255 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "folder_id": { "type": [ "integer", "null" ] } }, "required": [ "file" ] } } } }, "responses": { "201": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/FileResource" }, { "type": "object", "required": [ "folder", "uploaded_by", "categories" ] } ] } }, "required": [ "data" ] } } } }, "403": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "500": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/files/{file}": { "get": { "operationId": "files.show", "description": "Requires a token with any of these abilities: `upload`, `edit_files`, `edit_others_files`.", "tags": [ "Files" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FileResource" } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "patch": { "operationId": "files.update", "description": "Only the fields present in the request are changed; omitting one\nleaves it as it was.\n\nSome fields need a permission of their own \u2014 `expires_at` needs\n`set_file_expiration_date`, `public` needs `upload_public`, and\n`categories` needs `set_file_categories`. Sending one of those\nwithout the matching permission leaves that field untouched rather\nthan failing the whole request, which mirrors the web interface.\n\n`expires_at` accepts either a calendar day (`2026-09-12`) or a full\ntimestamp. A day means the end of that day in the caller's timezone,\nwhich is what the same value means on the web and what the file's\nown `expires_at` reads back as; a timestamp is taken as the instant\nit names.\n\n`commentable` only has an effect while the installation's comment\nsetting is \"only files marked as commentable\"; under any other\nsetting it is ignored, again rather than failing.\n\nRequires a token with any of these abilities: `edit_files`, `edit_others_files`.", "summary": "Update a file's metadata", "tags": [ "Files" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "folder_id": { "type": [ "integer", "null" ] }, "public": { "type": "boolean" }, "commentable": { "type": "boolean" }, "slug": { "type": [ "string", "null" ], "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 255 }, "categories": { "type": "array", "items": { "type": "integer" } }, "expires_at": { "type": [ "string", "null" ], "format": "date-time" }, "download_limit": { "type": [ "integer", "null" ], "minimum": 1 }, "download_limit_scope": { "$ref": "#/components/schemas/DownloadLimitScope" } } } } } }, "responses": { "200": { "description": "`FileResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/FileResource" }, { "type": "object", "required": [ "folder", "uploaded_by", "categories" ] } ] } }, "required": [ "data" ] } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "422": { "$ref": "#/components/responses/ValidationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "files.destroy", "description": "Requires a token with any of these abilities: `delete_files`, `delete_others_files`.", "tags": [ "Files" ], "parameters": [ { "name": "file", "in": "path", "required": true, "description": "The file ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "403": { "$ref": "#/components/responses/AuthorizationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/groups/{group}/members": { "post": { "operationId": "groups.members.store", "description": "Requires a token with the ability: `edit_groups`.", "tags": [ "GroupMembers" ], "parameters": [ { "name": "group", "in": "path", "required": true, "description": "The group ID", "schema": { "type": "integer" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "user_id": { "type": "integer" } }, "required": [ "user_id" ] } } } }, "responses": { "200": { "description": "`GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/GroupResource" }, { "type": "object", "required": [ "members" ] } ] } }, "required": [ "data" ] } } } }, "403": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/groups/{group}/members/{member}": { "delete": { "operationId": "groups.members.destroy", "description": "Requires a token with the ability: `edit_groups`.", "tags": [ "GroupMembers" ], "parameters": [ { "name": "group", "in": "path", "required": true, "description": "The group ID", "schema": { "type": "integer" } }, { "name": "member", "in": "path", "required": true, "description": "The member ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/GroupResource" }, { "type": "object", "required": [ "members" ] } ] } }, "required": [ "data" ] } } } }, "403": { "description": "An error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview.", "examples": [ "" ] } }, "required": [ "message" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/groups": { "get": { "operationId": "groups.index", "description": "Requires a token with the ability: `manage_groups`.", "tags": [ "Groups" ], "parameters": [ { "name": "updated_since", "in": "query", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "per_page", "in": "query", "schema": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": [ "string", "null" ] } }, { "name": "search", "in": "query", "schema": { "type": [ "string", "null" ], "maxLength": 255 } }, { "name": "visibility", "in": "query", "schema": { "type": [ "string", "null" ], "enum": [ "public", "private", null ] } } ], "responses": { "200": { "description": "Paginated set of `GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/GroupResource" } }, "links": { "type": "object", "properties": { "first": { "type": [ "string", "null" ] }, "last": { "type": [ "string", "null" ] }, "prev": { "type": [ "string", "null" ] }, "next": { "type": [ "string", "null" ] } }, "required": [ "first", "last", "prev", "next" ] }, "meta": { "type": "object", "properties": { "path": { "type": [ "string", "null" ], "description": "Base path for paginator generated URLs." }, "per_page": { "type": "integer", "description": "Number of items shown per page.", "minimum": 0 }, "next_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the next set of items." }, "prev_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the previous set of items." } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "links", "meta" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "post": { "operationId": "groups.store", "description": "Requires a token with the ability: `create_groups`.", "tags": [ "Groups" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "slug": { "type": [ "string", "null" ], "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 255 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "public": { "type": "boolean" } }, "required": [ "name", "public" ] } } } }, "responses": { "201": { "description": "`GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/GroupResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/groups/{group}": { "get": { "operationId": "groups.show", "description": "Requires a token with the ability: `edit_groups`.", "tags": [ "Groups" ], "parameters": [ { "name": "group", "in": "path", "required": true, "description": "The group ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "allOf": [ { "$ref": "#/components/schemas/GroupResource" }, { "type": "object", "required": [ "members" ] } ] } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "patch": { "operationId": "groups.update", "description": "Requires a token with the ability: `edit_groups`.", "tags": [ "Groups" ], "parameters": [ { "name": "group", "in": "path", "required": true, "description": "The group ID", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "slug": { "type": [ "string", "null" ], "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", "maxLength": 255 }, "description": { "type": [ "string", "null" ], "maxLength": 2000 }, "public": { "type": "boolean" } } } } } }, "responses": { "200": { "description": "`GroupResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/GroupResource" } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "groups.destroy", "description": "Requires a token with the ability: `delete_groups`.", "tags": [ "Groups" ], "parameters": [ { "name": "group", "in": "path", "required": true, "description": "The group ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/me": { "get": { "operationId": "me", "tags": [ "Me" ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "type": { "type": "string" }, "role": { "type": [ "string", "null" ] }, "locale": { "type": [ "string", "null" ] }, "abilities": { "type": "array", "description": "The *effective* set, not the token's stored list: a token\nminted before a demotion still carries abilities its owner\nhas since lost, and reporting those would tell an\nintegration it can do things every request will refuse.\nEnsureTokenCan enforces exactly this intersection.", "items": {} }, "token": { "type": "object", "properties": { "name": { "type": "string" }, "expires_at": { "type": [ "string", "null" ] } }, "required": [ "name", "expires_at" ] } }, "required": [ "id", "name", "email", "type", "role", "locale", "abilities", "token" ] }, "meta": { "type": "object", "properties": { "edition": { "type": "string" }, "capabilities": { "type": "array", "items": {} }, "modules": { "type": "array", "items": { "type": "string" } }, "version": { "type": "string" } }, "required": [ "edition", "capabilities", "modules", "version" ] } }, "required": [ "data", "meta" ] } } } }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/roles": { "get": { "operationId": "roles.index", "description": "`client_scoped` roles are the ones for which `assigned_clients`\nmeans anything on a user; for any other role the assignment is\ncleared. `is_administrator` marks the roles that hold every\npermission, which only an administrator may hand out.\n\nThe Client system role is never listed: clients are not staff and\nare managed through `/clients`.\n\nRequires a token with the ability: `manage_users`.", "summary": "List the roles you may assign to a staff account", "tags": [ "Roles" ], "responses": { "200": { "description": "", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "is_system": { "type": "boolean" }, "is_administrator": { "type": "boolean" }, "client_scoped": { "type": "boolean" } }, "required": [ "id", "name", "is_system", "is_administrator", "client_scoped" ] } } }, "required": [ "data" ] } } } }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/users": { "get": { "operationId": "users.index", "description": "Clients never appear here \u2014 they are a different population with\ntheir own endpoints under `/clients`.\n\nRequires a token with the ability: `manage_users`.", "summary": "List staff accounts", "tags": [ "Users" ], "parameters": [ { "name": "updated_since", "in": "query", "schema": { "type": [ "string", "null" ], "format": "date-time" } }, { "name": "per_page", "in": "query", "schema": { "type": [ "integer", "null" ], "minimum": 1, "maximum": 100 } }, { "name": "cursor", "in": "query", "schema": { "type": [ "string", "null" ] } }, { "name": "search", "in": "query", "schema": { "type": [ "string", "null" ], "maxLength": 255 } }, { "name": "role_id", "in": "query", "schema": { "type": [ "integer", "null" ] } }, { "name": "status", "in": "query", "schema": { "type": [ "string", "null" ], "enum": [ "active", "inactive", null ] } } ], "responses": { "200": { "description": "Paginated set of `StaffUserResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/StaffUserResource" } }, "links": { "type": "object", "properties": { "first": { "type": [ "string", "null" ] }, "last": { "type": [ "string", "null" ] }, "prev": { "type": [ "string", "null" ] }, "next": { "type": [ "string", "null" ] } }, "required": [ "first", "last", "prev", "next" ] }, "meta": { "type": "object", "properties": { "path": { "type": [ "string", "null" ], "description": "Base path for paginator generated URLs." }, "per_page": { "type": "integer", "description": "Number of items shown per page.", "minimum": 0 }, "next_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the next set of items." }, "prev_cursor": { "type": [ "string", "null" ], "description": "The \"cursor\" that points to the previous set of items." } }, "required": [ "path", "per_page", "next_cursor", "prev_cursor" ] } }, "required": [ "data", "links", "meta" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "post": { "operationId": "users.store", "description": "`role_id` must name a role the calling token's owner could grant.\nYou cannot hand out authority you do not hold: a caller who is not\nan administrator may not create one, nor assign any role carrying a\npermission they lack. `GET /roles` lists what is available to you.\n\nThe account is created active and with its email already verified \u2014\nan administrator vouched for the address by sending it.\n\nRequires a token with the ability: `create_users`.", "summary": "Create a staff account", "tags": [ "Users" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "email": { "type": "string", "format": "email", "maxLength": 255 }, "role_id": { "type": "integer" }, "password": { "type": "string", "description": "No `confirmed`: repeating a password defends against a human\nmistyping into a form, and an API caller has no second field\nto mistype. Password::defaults() still applies." }, "assigned_clients": { "type": "array", "items": { "type": "integer", "description": "Only clients you can reach yourself: an unrestricted account may\nassign any client, a client-scoped one only the clients already\nassigned to it. Assigning a client hands over everything that\nclient can see, so it follows the same rule as role_id above." } } }, "required": [ "name", "email", "role_id", "password" ] } } } }, "responses": { "201": { "description": "`StaffUserResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/StaffUserResource" } }, "required": [ "data" ] } } } }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/users/{user}": { "get": { "operationId": "users.show", "description": "Adds `assigned_client_ids` and the `content` counts that `DELETE`\nneeds, neither of which the listing carries.\n\nRequires a token with the ability: `edit_users`.", "summary": "Get one staff account", "tags": [ "Users" ], "parameters": [ { "name": "user", "in": "path", "required": true, "description": "The user ID", "schema": { "type": "integer" } } ], "responses": { "200": { "description": "`StaffUserResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/StaffUserResource" } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "patch": { "operationId": "users.update", "description": "PATCH semantics: an absent key means \"leave alone\", not \"clear\".\nSending `assigned_clients` replaces the whole list; omitting it\nleaves it, except that moving to a role which is not client-scoped\nclears it either way.\n\nRefused with a 422 if the change would leave the installation with\nno active administrator, or if you would be deactivating yourself.\n\nRequires a token with the ability: `edit_users`.", "summary": "Update a staff account, including the role assigned to it", "tags": [ "Users" ], "parameters": [ { "name": "user", "in": "path", "required": true, "description": "The user ID", "schema": { "type": "integer" } } ], "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "name": { "type": "string", "maxLength": 255 }, "email": { "type": "string", "format": "email", "maxLength": 255 }, "role_id": { "type": "integer" }, "active": { "type": "boolean" }, "password": { "type": [ "string", "null" ] }, "assigned_clients": { "type": "array", "items": { "type": "integer", "description": "Only clients you can reach yourself: an unrestricted account may\nassign any client, a client-scoped one only the clients already\nassigned to it. Assigning a client hands over everything that\nclient can see, so it follows the same rule as role_id above." } } } } } } }, "responses": { "200": { "description": "`StaffUserResource`", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/StaffUserResource" } }, "required": [ "data" ] } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "422": { "$ref": "#/components/responses/ValidationException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } }, "delete": { "operationId": "users.destroy", "description": "If the account owns no files or folders, no body is needed.\n\nIf it does, you must say what happens to that content: send\n`content_action` as either `cascade_delete` (delete it along with\nthe account) or `reassign`, and in the latter case a\n`reassign_to_id` naming the active account that inherits it.\nOmitting the choice is a 422 \u2014 there is no default, because one\nwould silently destroy files and the other would silently hand them\nto somebody else. `GET /users/{user}` reports the counts.\n\nYou cannot delete your own account, and you cannot delete the last\nactive administrator.\n\nRequires a token with the ability: `delete_users`.", "summary": "Delete a staff account", "tags": [ "Users" ], "parameters": [ { "name": "user", "in": "path", "required": true, "description": "The user ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } }, "/users/{user}/two-factor": { "delete": { "operationId": "users.two-factor.destroy", "description": "The remedy for a locked-out account: somebody whose authenticator\napp and recovery codes are both gone cannot sign in, and nobody else\ncan open the account for them either. Afterwards the account signs\nin with its password alone, and \u2014 if this installation enforces\ntwo-factor authentication for staff \u2014 is asked to enrol again on its\nnext request.\n\nThe account holder is emailed that this happened, and the action is\nrecorded in the activity log against the caller. Answers 204 whether\nor not a second factor was actually in force.\n\nRequires a token with the ability: `edit_users`.", "summary": "Remove a staff account's two-factor authentication", "tags": [ "Users" ], "parameters": [ { "name": "user", "in": "path", "required": true, "description": "The user ID", "schema": { "type": "integer" } } ], "responses": { "204": { "description": "No content", "content": { "application/json": { "schema": { "type": "array", "items": {} } } } }, "404": { "$ref": "#/components/responses/ModelNotFoundException" }, "401": { "$ref": "#/components/responses/AuthenticationException" } } } } }, "components": { "securitySchemes": { "http": { "type": "http", "scheme": "bearer" } }, "schemas": { "Action": { "type": "string", "description": "Every action the activity log can record \u2014 v2's replacement for v1's ~45 numbered action types in ActionsLog. Modules add their own cases (file.uploaded, group.created, \u2026) as they land; the string values are stable identifiers stored in the database.\n", "enum": [ "setup.completed", "settings.updated", "application.updated", "auth.login", "auth.logout", "user.created", "user.updated", "user.deleted", "user.activated", "user.deactivated", "account.erased", "account_content.cascade_deleted", "account_content.reassigned", "account.converted_to_client", "account.converted_to_staff", "client.self_registered", "ldap.client_provisioned", "social.client_provisioned", "social.account_linked", "social.account_unlinked", "client.approved", "client.denied", "file.uploaded", "file.updated", "file.deleted", "file.downloaded", "file.previewed", "file.assigned", "file.unassigned", "file.version_linked", "file.version_unlinked", "share_link.created", "share_link.revoked", "share_link.downloaded", "public_file.downloaded", "public_file.previewed", "folder.created", "folder.renamed", "folder.moved", "folder.deleted", "folder.shared", "folder.unshared", "file.made_public", "file.made_private", "folder.made_public", "folder.made_private", "upload.aborted", "file.imported", "orphan_file.deleted", "orphan_file.auto_deleted", "file.expired_deleted", "group.membership_left", "group.membership_requested", "group.membership_approved", "group.membership_denied", "group.created", "group.updated", "group.deleted", "group.made_public", "group.made_private", "group.member_added", "group.member_removed", "role.created", "role.updated", "role.deleted", "category.created", "category.renamed", "category.deleted", "client_custom_field.created", "client_custom_field.updated", "client_custom_field.deleted", "profile.updated", "password.updated", "two_factor.enabled", "two_factor.disabled", "two_factor.recovery_codes_regenerated", "two_factor.reset", "api_token.created", "api_token.updated", "api_token.revoked", "custom_asset.created", "custom_asset.updated", "custom_asset.deleted", "custom_asset.enabled", "custom_asset.disabled", "comment.posted", "comment.posted_by_visitor", "comment.edited", "comment.deleted", "comment.approved" ], "title": "Action" }, "ActivityResource": { "type": "object", "properties": { "id": { "type": "integer" }, "action": { "type": "string" }, "created_at": { "type": "string" }, "actor": { "type": [ "object", "null" ], "description": "Snapshots, not joins. The actor may since have been deleted,\nand the entry still has to say who it was.", "properties": { "id": { "type": [ "integer", "null" ] }, "name": { "type": [ "string", "null" ] }, "type": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "type" ] }, "origin": { "type": "string", "description": "How it arrived: a person in the browser, an integration, a\nvisitor with no account, or the installation itself." }, "subject": { "type": [ "object", "null" ], "properties": { "type": { "anyOf": [ { "type": "string" }, { "type": "string", "enum": [ "other" ] } ] }, "id": { "type": [ "integer", "null" ] }, "name": { "type": [ "string", "null" ] } }, "required": [ "type", "id", "name" ] }, "context": { "type": "array", "description": "Whatever the action recorded beyond its subject \u2014 who a file\nwas shared with, how many files a cascade removed. Shape\nvaries by action and is documented per action rather than\nhere.", "items": {} } }, "required": [ "id", "action", "created_at", "actor", "origin", "subject", "context" ], "title": "ActivityResource" }, "ClientResource": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "active": { "type": "boolean" }, "account_requested": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean", "description": "Whether, not what: the state of the second factor is what a\ncaller needs to see before removing it. The secret and the\nrecovery codes stay where they are." }, "created_at": { "type": [ "string", "null" ] }, "updated_at": { "type": [ "string", "null" ] }, "storage": { "type": "object", "properties": { "quota_mb": { "type": "integer", "description": "The client's own column, where 0 means \"inherit the site\n default\" rather than \"unlimited\" \u2014 both are reported so a\ncaller need not know that rule to display it correctly." }, "effective_quota_mb": { "type": "integer" }, "unlimited": { "type": "boolean" }, "used_mb": { "type": "integer" } }, "required": [ "quota_mb", "effective_quota_mb", "unlimited", "used_mb" ] }, "custom_fields": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "label": { "type": "string" }, "type": { "type": "string" }, "value": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "label", "type", "value" ] } }, "content": { "type": "string" } }, "required": [ "id", "name", "email", "active", "account_requested", "two_factor_enabled", "created_at", "updated_at", "storage", "custom_fields", "content" ], "title": "ClientResource" }, "CommentVisibility": { "type": "string", "description": "Who a comment is addressed to. Four fixed audiences, and the list never changes \u2014 it does not grow with the number of clients a file is shared with. An earlier version made staff pick a recipient from a dropdown of every client on the file before they could write, which is not how anybody answers a message and reached 51 entries on a file shared with a 50-client group. Answering one client is a reply now (see FileComments::post's $replyTo), so the audience of a reply is inherited from the comment it answers rather than chosen. **`Clients` includes staff.** The name says who it *adds* \u2014 the team can already see everything on a file they can open. Read it as \"the team and * the clients\", never as \"clients instead of staff\"; the label a person sees says so out loud, because the short name did not and somebody reasonably concluded there was no staff-and-clients option at all. It is one channel seen from two ends, which is why its label depends on who is reading: staff address \"Staff and clients\", a client addresses \"Staff\". That asymmetry is real, not cosmetic \u2014 a client's comment carries their own conversation and never fans out to the other clients, so for them the audience genuinely is the team alone. Narrowing to a single client is `client_context_id`, not a separate case: see Access\\VisibleCommentScope. The case names avoid `Private`/`Public`: those words already mean something else here (a *file's* public flag, and CommentScope's `public_files`), and reading `CommentVisibility::Public` next to `$file->public` invites exactly the confusion this feature cannot afford.\n", "enum": [ "only_me", "staff_only", "clients", "everyone" ], "title": "CommentVisibility" }, "DownloadLimitScope": { "type": "string", "description": "What a file's download limit counts. `Total` caps the file itself: once it has been downloaded that many times by anyone, nobody may download it again. `PerUser` gives every person their own allowance, so one client exhausting theirs leaves everyone else untouched. Both come from ProjectSend v1, which stored them in `tbl_files.download_limit_type` under these exact names \u2014 an imported install keeps behaving the way its administrator set it up. A closed set on purpose: DownloadAllowance branches on it, and a third value arriving from a database someone edited by hand should fail loudly at the cast rather than silently fall through to \"no limit\".\n", "enum": [ "total", "per_user" ], "title": "DownloadLimitScope" }, "FileCommentResource": { "type": "object", "properties": { "id": { "type": "integer" }, "file_id": { "type": "integer" }, "body": { "type": "string" }, "visibility": { "type": "string" }, "author": { "type": "object", "properties": { "id": { "type": [ "integer", "null" ] }, "name": { "type": "string" }, "type": { "type": "string", "enum": [ "guest", "staff", "client" ] } }, "required": [ "id", "name", "type" ] }, "conversation": { "type": [ "object", "null" ], "description": "Whose conversation this is, when it is one client's rather\nthan every client's. Staff only, and null on a comment\naddressed to all of them.", "properties": { "client_id": { "type": [ "integer", "null" ] }, "client_name": { "type": [ "string", "null" ] } }, "required": [ "client_id", "client_name" ] }, "approved": { "type": "boolean" }, "created_at": { "type": [ "string", "null" ] }, "edited_at": { "type": [ "string", "null" ] } }, "required": [ "id", "file_id", "body", "visibility", "author", "conversation", "approved", "created_at", "edited_at" ], "title": "FileCommentResource" }, "FileResource": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "slug": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "original_name": { "type": "string" }, "mime_type": { "type": "string" }, "size": { "type": "integer" }, "checksum": { "type": "string" }, "public": { "type": "boolean" }, "commentable": { "type": "boolean", "description": "Only consulted while the installation's comment setting is\n\"only files marked as commentable\"; published anyway, since a\ncaller that sets it wants to read it back." }, "expires_at": { "type": [ "string", "null" ] }, "expired": { "type": "boolean" }, "download_limit": { "type": [ "integer", "null" ], "description": "Null when the file may be downloaded any number of times.\n`download_limit_scope` says what the number counts \u2014\n\"total\" across everyone, or \"per_user\" for each person\nseparately. It is always one of those two, and is\nmeaningless while the limit is null." }, "download_limit_scope": { "type": "string" }, "downloads_used": { "type": "integer", "description": "The file's total downloads. Under a per_user limit this is\nstill the total, since \"how much has this caller used\" is a\ndifferent number for every caller.", "minimum": 0 }, "created_at": { "type": [ "string", "null" ] }, "updated_at": { "type": [ "string", "null" ] }, "is_revision": { "type": "boolean", "description": "True when this file is a new version of an earlier one. A\nrevision is always shared with the same people as the file it\nreplaces, so it has no recipients of its own: assigning it\nreturns 422, and `sharing_root_id` names the file to assign\ninstead." }, "sharing_root_id": { "type": [ "integer", "null" ], "description": "The oldest file in this version chain \u2014 the one whose\nrecipients govern the whole chain. Null when this file is not\na revision." }, "previous_version": { "type": [ "object", "null" ], "description": "The file this one replaces. Null when there is none, or when\nit is one you cannot see.", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "next_version": { "type": [ "object", "null" ], "description": "The file that replaced this one, on the same terms.", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "folder": { "type": [ "object", "null" ], "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "uploaded_by": { "type": [ "object", "null" ], "description": "Name only. The uploader is a user record; their email address\nis not part of what \"this file exists\" needs to say. Null\nwhen the uploader is a client the token's owner is not\nscoped to; an unscoped account always gets the name.", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] }, "categories": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" } }, "required": [ "id", "name" ] } }, "assignments": { "type": "array", "description": "Who the file is shared with, as far as this caller is\nconcerned: a recipient the token's owner is not scoped to is\nleft out rather than returned without a name.", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "group", "client" ] }, "id": { "type": "integer" }, "name": { "description": "getAttribute() rather than ->name: the relation is a\nMorphTo over User|Group, so the property is only\nknowable at runtime. Both targets carry a name." } }, "required": [ "type", "id", "name" ] } }, "links": { "type": "object", "properties": { "download": { "type": "string" } }, "required": [ "download" ] } }, "required": [ "id", "name", "slug", "description", "original_name", "mime_type", "size", "checksum", "public", "commentable", "expires_at", "expired", "download_limit", "download_limit_scope", "downloads_used", "created_at", "updated_at", "is_revision", "sharing_root_id", "links" ], "title": "FileResource" }, "GroupResource": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "slug": { "type": "string" }, "description": { "type": [ "string", "null" ] }, "public": { "type": "boolean" }, "members_count": { "type": "integer" }, "created_at": { "type": [ "string", "null" ] }, "updated_at": { "type": [ "string", "null" ] }, "members": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" } }, "required": [ "id", "name", "email" ] } } }, "required": [ "id", "name", "slug", "description", "public", "created_at", "updated_at" ], "title": "GroupResource" }, "StaffUserResource": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string" }, "active": { "type": "boolean" }, "two_factor_enabled": { "type": "boolean" }, "role": { "type": [ "object", "null" ], "description": "Nested rather than a bare role_id, so a caller can render a\nlist without a second request. `is_administrator` is the flag\nthat actually matters when reading one of these.", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "is_system": { "type": "boolean" }, "is_administrator": { "type": "boolean" }, "client_scoped": { "type": "boolean" } }, "required": [ "id", "name", "is_system", "is_administrator", "client_scoped" ] }, "created_at": { "type": [ "string", "null" ] }, "updated_at": { "type": [ "string", "null" ] }, "assigned_client_ids": { "type": "string" }, "content": { "type": "string" } }, "required": [ "id", "name", "email", "active", "two_factor_enabled", "role", "created_at", "updated_at", "assigned_client_ids", "content" ], "title": "StaffUserResource" } }, "responses": { "AuthenticationException": { "description": "Unauthenticated", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview." } }, "required": [ "message" ] } } } }, "ValidationException": { "description": "Validation error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Errors overview." }, "errors": { "type": "object", "description": "A detailed description of each field that failed validation.", "additionalProperties": { "type": "array", "items": { "type": "string" } } } }, "required": [ "message", "errors" ] } } } }, "AuthorizationException": { "description": "Authorization error", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview." } }, "required": [ "message" ] } } } }, "ModelNotFoundException": { "description": "Not found", "content": { "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string", "description": "Error overview." } }, "required": [ "message" ] } } } } } } }