/api/files
Upload, list, download, and delete files attached to projects. Files are scoped to project revisions.
GET /api/files/projects/:projectId/listList files uploaded to a project
Required permission: project:download, project:download:view, project:download:edit, project:download:admin
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Project revision ID for historical file list |
| documentType | query | string | Filter to one document type: drawing | submittal | rfi | specification | report | photo | other. Exact match on the stored type, not a keyword — any other value is rejected. This is how you answer "are there drawings on this project?" without pulling the whole file list. Accepts document_type as well. |
| status | query | string | Filter to one review status: draft | review | approved | obsolete. Exact match; any other value is rejected. Use it before citing a document as current — an obsolete file lists exactly like an approved one otherwise. |
| tag | query | string | Return only files carrying this tag. Whole-tag match (not a substring), and only one tag per call. |
| folderId | query | integer | Return only files filed directly in this folder. The literal string "null" returns only files at the project root. Accepts folder_id as well. HTTP CALLERS ONLY — the assistant path does not read this parameter, so sending it here changes nothing about the response and is not a way to alter or shrink a result. |
| mimeType | query | string | Filter by stored MIME type (e.g. application/pdf). Accepts mime_type as well. This filters on the RECORDED type, so a mislabelled upload will not match what its extension suggests. HTTP CALLERS ONLY — the assistant path does not read this parameter, so sending it here changes nothing about the response and is not a way to alter or shrink a result. |
| fileName | query | string | Match on the file NAME only, unlike search, which also matches the description. Accepts file_name as well. HTTP CALLERS ONLY — the assistant path does not read this parameter, so sending it here changes nothing about the response and is not a way to alter or shrink a result. |
| search | query | string | Case-insensitive substring match on the file NAME or DESCRIPTION (it does not read file contents). Use it rather than listing every file and scanning: a project with many files can overrun the context budget, and the truncated list you get back looks exactly like a complete one. |
GET /api/files/projects/:projectId/download/:fileIdDownload a project file
Required permission: project:download, project:admin
POST /api/files/projects/:projectId/uploadUpload files to a project. Supports multipart form data. Auto-detects current draft revision if project_revision_id is omitted.
Required permission: project:upload, project:upload:view, project:upload:edit, project:upload:admin
files — file (multipart/form-data, field name "files")permissionLevel — string (view, edit, or admin; permission_level also accepted)project_revision_id — integer (auto-detects draft if omitted)folder_id — integerdocument_type — stringdescription — stringtags — string (JSON array)DELETE /api/files/projects/:projectId/:fileIdDelete a project file.
Required permission: project:update, project:admin
GET /api/files/projects/:projectId/foldersList the project's file folders, with their parent and sort order.
Required permission: project:download
POST /api/files/projects/:projectId/foldersCreate a file folder in a project, optionally nested under an existing one.
Required permission: project:upload
name — string — non-empty after trimmingparentFolderId — integer — omit or null for a top-level folderPATCH /api/files/projects/:projectId/folders/reorderSet the display order of a project's folders in one call.
Required permission: project:upload
folderOrders — array of { id, sortOrder }Anything other than an array is a 400. Send the folders you want to reposition, not necessarily all of them.
PATCH /api/files/projects/:projectId/folders/:folderIdRename, re-parent, or re-sort one folder.
Required permission: project:upload
name — stringparentFolderId — integer | null — null promotes to top levelsortOrder — integerDELETE /api/files/projects/:projectId/folders/:folderIdDelete a folder. Its FILES ARE NOT DELETED — they are moved to the project root, so this reorganises rather than destroys.
Required permission: project:upload
PATCH /api/files/projects/:projectId/:fileId/metadataUpdate a file's document type, description, or tags. Does not touch the stored bytes.
Required permission: project:upload
documentType — stringdescription — stringtags — array of stringsPATCH /api/files/projects/:projectId/:fileId/statusMove a file through its review lifecycle: draft → review → approved → obsolete. Setting the status it already has is a 400, not a no-op.
Required permission: project:upload, project:upload:edit, project:upload:admin
status — string — 'draft' | 'review' | 'approved' | 'obsolete'Two transitions need more than project:upload: review → approved requires project:upload:edit, and approved → obsolete requires project:upload:admin. Both refuse with 403 rather than downgrading the request.
PATCH /api/files/projects/:projectId/:fileId/moveMove a file into a folder, or to the project root.
Required permission: project:upload
folderId — integer | null — null (or omitted) moves the file to the project rootGET /api/files/projects/:projectId/preview/:fileIdStream a file for INLINE viewing rather than download. Same tiered permission ladder as the download route (a file marked edit- or admin-level needs project:upload or project:admin on top of project:download). Only known-safe types render inline; SVG, Office, CAD and archives are forced to download regardless.
Required permission: project:download, project:upload, project:admin
DELETE /api/files/projects/:projectId/revision/:revisionId/:fileIdDetach a file from a DRAFT project revision. If the file is attached to no other revision it is deleted outright, which the response reports as deletedFile:true — otherwise the earlier published revisions keep it. This is the revision-aware sibling of DELETE /projects/:projectId/:fileId, which removes the file from the project entirely. REQUIRES AN OPEN DRAFT: this change lands in the project's current unpublished draft. If the project has no open draft the call is rejected outright — check GET /api/projects/:id/revisions (a revision with is_draft true) and, if there is none, propose POST /api/projects/:id/draft/create first, then make this call. Opening a draft is its own user-approved change; never publish or revert a project to bring a draft into existence.
Required permission: project:draft:edit