Files & Upload API
Upload files, manage stored files, and search across all resources. File uploads support linking to submissions and matters, include server-side security validation, and persist file binaries in a configurable storage backend. The default backend stores encrypted bytes in PostgreSQL; self-hosted deployments can use an S3-compatible object store such as MinIO on the same box so files stay local while the database remains the authoritative index.
Endpoints
| Method | Endpoint | Description |
|---|---|---|
GET | /api/files | List uploaded files |
POST | /api/files | Upload a standalone file (files workspace) |
GET | /api/files/browse | Browse files with extraction status, quality scores, and entity filtering |
GET | /api/files/browse/structure | Get smart folder structure grouped by linked entities |
PUT | /api/files/:fileId/star | Toggle starred status on a file |
PUT | /api/files/:fileId/move | Move a file to a folder |
POST | /api/files/folders | Create a folder |
PUT | /api/files/folders | Rename a folder |
DELETE | /api/files/folders | Delete a folder |
POST | /api/upload | Upload a file and optionally link it to a submission or matter |
GET | /api/search | Global search across forms, submissions, tables, matters, boards, documents, records, workflows, and pages |
Storage Backends
File metadata (filename, MIME type, size, sensitivity, links) always lives in the Opbox database. The actual bytes can be stored in one of three backends, selected by the FILE_STORAGE_BACKEND environment variable:
| Backend | FILE_STORAGE_BACKEND | Where bytes live | Best for |
|---|---|---|---|
| Database | db (default) | StoredFile.content in PostgreSQL, AES-256-GCM encrypted when ENCRYPTION_KEY is set | Small deployments, simplicity, no extra service |
| S3-compatible | s3 | S3-compatible object store (MinIO, Garage, Ceph, R2, etc.) | Larger files, separation of blob cost from database, same-box self-hosting |
| Disk | disk | Local filesystem under STORAGE_DIR | Legacy fallback and local dev |
S3 / MinIO on the same box
For Azure, on-prem, or security-conscious deployments, run an S3-compatible store on the same machine as Opbox. All file bytes stay on infrastructure you control; the database only keeps the index.
Set these environment variables:
| Variable | Required | Description |
|---|---|---|
FILE_STORAGE_BACKEND | Yes | s3 |
S3_ENDPOINT | Yes | Endpoint URL, e.g. http://127.0.0.1:9000 for local MinIO or a private cable/tailnet IP |
S3_REGION | Yes | S3 region, e.g. us-east-1 |
S3_BUCKET | Yes | Bucket name, e.g. opbox-files |
S3_ACCESS_KEY_ID | Yes | Access key |
S3_SECRET_ACCESS_KEY | Yes | Secret key |
S3_FORCE_PATH_STYLE | No | true (default) for MinIO; false for virtual-hosted-style clouds |
S3_USE_TLS | No | true (default) to require TLS |
S3_PRESIGNED_URL_EXPIRY_SECONDS | No | Presigned URL lifetime in seconds (default 300) |
S3_SSE_CUSTOMER_KEY | No | Optional per-object server-side encryption key |
ENCRYPTION_KEY | Recommended | Encrypts bytes before they leave the app, even if the object store is compromised |
Matter-scoped S3 prefixes
When a file is uploaded for a matter, the S3 key is scoped under the matter number so the object store mirrors the workspace filing structure:
matters/{matterNumber}/{folder}/{timestamp}_{random}_{filename}
For example:
matters/MAT-0001/1. Engagement/Letters/mqknd7k8_letter.pdf
Use the matterId and folder fields when uploading through POST /api/upload to opt into this layout. Files uploaded without a matter still use the default flat key format.
List Files
Returns all uploaded files for your workspace.
GET /api/files?search=passport&type=documents
Query Parameters
| Parameter | Type | Description |
|---|---|---|
search | string | Filter by filename substring |
type | string | Type filter: images, documents, spreadsheets, archives |
Response
{
"files": [
{
"id": "cm_attachment_abc123",
"filename": "passport-scan.pdf",
"fileUrl": "/api/documents/5f9d6c13e2_mi2f6b_4sB...pdf",
"fileSize": 245760,
"mimeType": "application/pdf",
"createdAt": "2026-02-13T09:30:00.000Z",
"submission": {
"id": "SUB-8XQ2M7K9P1ZT",
"formTitle": "Client Onboarding",
"user": {
"id": "cm_user_123",
"name": "Jane Smith"
}
}
}
],
"stats": {
"totalFiles": 56,
"totalSize": 9830400,
"fileTypes": {
"pdf": 20,
"png": 11
}
}
}
Upload via Files Workspace
Uploads a standalone file and links it to the system File Uploads submission.
POST /api/files
Content-Type: multipart/form-data
------boundary
Content-Disposition: form-data; name="file"; filename="company-logo.png"
Content-Type: image/png
<binary data>
------boundary--
Response
{
"success": true,
"file": {
"id": "cm_attachment_def456",
"filename": "company-logo.png",
"fileUrl": "/api/documents/5f9d6c13e2_mi2f70_EJd...png",
"fileSize": 82944,
"mimeType": "image/png"
}
}
Browse Files
Returns files for your workspace with extraction status, quality scores, and entity link information. Supports filtering by entity, folder, and special views.
GET /api/files/browse?search=passport&entityId=cm_row_rec1&entityType=ROW
Query Parameters
| Parameter | Type | Description |
|---|---|---|
search | string | Filter by filename substring |
type | string | Type filter: images, documents, spreadsheets, archives |
entityId | string | Filter to files linked to a specific entity (e.g. a record row ID) |
entityType | string | Entity type filter, used with entityId: ROW, MATTER, SUBMISSION |
linkTableId | string | Filter to files linked to rows in a specific table |
recent | boolean | Set to true to show recently uploaded files |
starred | boolean | Set to true to show only starred files |
unlinked | boolean | Set to true to show only files not linked to any entity |
Response
Each file includes optional extraction and quality fields. The extraction object shows the document extraction pipeline status (PENDING, PROCESSING, COMPLETED, FAILED, or TIMED_OUT) and completion timestamp. The quality object includes a structureScore (0-1) and a passed boolean. Both fields are null when not yet available.
{
"files": [
{
"id": "cm_file_abc123",
"filename": "passport-scan.pdf",
"fileUrl": "/api/documents/5f9d6c13e2_mi2f6b_4sB...pdf",
"fileSize": 245760,
"mimeType": "application/pdf",
"starred": false,
"folderId": null,
"createdAt": "2026-02-13T09:30:00.000Z",
"extraction": {
"status": "COMPLETED",
"completedAt": "2026-02-13T09:31:12.000Z"
},
"quality": {
"structureScore": 0.92,
"passed": true
},
"links": [
{
"entityId": "cm_row_rec1",
"entityType": "ROW",
"tableId": "cm_table_456"
}
]
}
],
"total": 56
}
Browse Folder Structure
Returns a smart folder structure for the file browser sidebar. Files are grouped into sections by linked entity type (e.g. Individuals, Matters), with counts per entity. Also returns totals for the Recent, Starred, and Unlinked virtual folders.
GET /api/files/browse/structure
Response
{
"sections": [
{
"key": "clients",
"label": "Individuals",
"icon": "Users",
"totalFiles": 12,
"entities": [
{
"id": "cm_row_rec1",
"name": "Acme Corp",
"count": 4,
"tableId": "cm_table_456"
}
]
}
],
"recent": 8,
"starred": 3,
"unlinked": 5
}
Toggle Star
Toggles the starred status of a file. Starred files appear in the Starred virtual folder in the file browser.
PUT /api/files/:fileId/star
Response
{
"success": true,
"starred": true
}
Create Folder
Creates a new folder for organising files.
POST /api/files/folders
Content-Type: application/json
{
"name": "Client Documents"
}
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | The folder name |
Response
{
"id": "cm_folder_abc123",
"name": "Client Documents",
"organizationId": "cm_org_123",
"createdAt": "2026-03-01T10:00:00.000Z"
}
Rename Folder
Renames an existing folder.
PUT /api/files/folders
Content-Type: application/json
{
"id": "cm_folder_abc123",
"name": "Renamed Folder"
}
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The folder ID |
name | string | Yes | The new folder name |
Delete Folder
Deletes a folder. Files inside the folder are moved to the root level (not deleted).
DELETE /api/files/folders
Content-Type: application/json
{
"id": "cm_folder_abc123"
}
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | The folder ID to delete |
Move File to Folder
Moves a file into a folder, or back to the root level by passing null as the folder ID.
PUT /api/files/:fileId/move
Content-Type: application/json
{
"folderId": "cm_folder_abc123"
}
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
folderId | string | null | Yes | Target folder ID, or null to move to root |
Response
{
"success": true,
"file": {
"id": "cm_file_abc123",
"folderId": "cm_folder_abc123"
}
}
Upload a File
Uploads a file using multipart/form-data encoding. Files can optionally be linked to a submission by passing submissionId, or scoped to a matter by passing matterId and an optional folder path.
POST /api/upload
Content-Type: multipart/form-data
------boundary
Content-Disposition: form-data; name="file"; filename="passport-scan.pdf"
Content-Type: application/pdf
<binary data>
------boundary
Content-Disposition: form-data; name="submissionId"
SUB-8XQ2M7K9P1ZT
------boundary
Content-Disposition: form-data; name="matterId"
cm_matter_789
------boundary
Content-Disposition: form-data; name="folder"
1. Engagement/Letters
------boundary--
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
file | File | Yes | The file to upload |
submissionId | string | No | Links the uploaded file to a specific submission |
matterId | string | No | When set, the file is linked to the matter and the S3 key is scoped under matters/{matterNumber}/{folder}/ |
folder | string | No | Matter folder path, used when matterId is set. Forward-slash separated, e.g. 1. Engagement/Letters |
Response
{
"success": true,
"file": {
"key": "5f9d6c13e2_mi2f6b_4sB...pdf",
"filename": "passport-scan.pdf",
"size": 245760,
"mimeType": "application/pdf",
"url": "/api/documents/5f9d6c13e2_mi2f6b_4sB...pdf",
"attachmentId": "cm_attachment_abc123"
}
}
Upload Security
- Max file size: 10MB by default (configurable by the workspace administrator).
- MIME type validation: Dangerous executable/script MIME types are blocked (for example PE binaries, shell scripts, HTML, and JavaScript MIME types).
- Content inspection: Server-side validation ensures that the file’s actual content matches its declared MIME type. Files masquerading as images (e.g. SVG with
image/pngheader) are rejected. - Content-Disposition: Markup-like MIME types such as
image/svg+xmlare forced to download viaContent-Disposition: attachment. - Security token: File uploads require authentication.
- Encryption at rest: When
ENCRYPTION_KEYis set, files are encrypted with AES-256-GCM before storage. For the S3 backend, an optionalS3_SSE_CUSTOMER_KEYadds server-side encryption on the object store. - Same-box S3: The S3-compatible backend is designed to run on the same host as Opbox (e.g. MinIO on
localhostor a private tailnet/cable IP), so file bytes never traverse the public internet. Bind the object store to a private interface, require TLS withS3_USE_TLS=true, and rotate credentials regularly.
Blocked MIME Types
| MIME Type | Reason |
|---|---|
application/x-msdownload | Native executable binary |
application/x-sh | Shell script execution risk |
text/html | HTML can execute scripts when rendered inline |
application/javascript | Script execution risk |
Global Search
Searches across all resource types in your workspace. Results are grouped by type: forms, submissions, tables, matters, boards, documents, records, workflows, and navigation pages.
GET /api/search?q=onboarding
Query Parameters
| Parameter | Type | Description |
|---|---|---|
q | string | Search query string (required). Searches titles, names, descriptions, tags, and data content. |
entities | string | Comma-separated list of entity types to search. Defaults to all types. Values: forms, submissions, tables, workflows, templates, boards, matters, documents, records, pages |
limit | number | Maximum results to return (default: 20) |
sortBy | string | Sort order: relevance (default) or date |
sortOrder | string | Sort direction: desc (default) or asc |
Response
{
"results": {
"forms": [
{
"id": "cm_form_123",
"title": "Client Onboarding",
"description": "Collect client details for onboarding",
"status": "PUBLISHED"
}
],
"submissions": [
{
"id": "SUB-8XQ2M7K9P1ZT",
"formTitle": "Client Onboarding",
"status": "PENDING",
"createdAt": "2026-02-12T09:15:00.000Z"
}
],
"tables": [
{
"id": "cm_table_456",
"name": "Individuals",
"category": "SYSTEM"
}
],
"matters": [
{
"id": "cm_matter_789",
"number": "MAT-0012",
"title": "Acme Corp Onboarding",
"status": "IN_PROGRESS",
"priority": "HIGH",
"boardName": "Client Onboarding"
}
],
"boards": [
{
"id": "cm_template_abc",
"title": "Client Onboarding",
"status": "ACTIVE"
}
],
"documents": [
{
"id": "cm_row_doc1",
"title": "Onboarding Checklist",
"tableName": "Documents"
}
],
"records": [
{
"id": "cm_row_rec1",
"title": "Acme Corp",
"tableName": "Companies"
}
],
"pages": [
{
"id": "page-matters",
"title": "Matters",
"url": "/matters"
}
],
"workflows": []
},
"query": "onboarding",
"totalResults": 8,
"durationMs": 12
}
Search Result Groups
| Group | Searches |
|---|---|
forms | Form title and description |
submissions | Submission ID, form title, submitter name/email, and data content |
tables | Table name and description |
matters | Matter number, title, tags, and board name |
boards | Board (matter template) name and description |
documents | Document title from the system Documents table |
records | CRM and user table rows - searches all field data (Individuals, Companies, etc.) |
workflows | Workflow name and description |
pages | Navigation pages and settings (e.g. “dark mode” matches Appearance) |
Entity File Intelligence
Deep file aggregation endpoints for CSP entities and individuals. These endpoints traverse company rows, linked matters, and stakeholder relationships to provide a complete file picture for a given entity.
Entity Files (Company)
Deep file aggregation for a CSP entity (company). Returns files from direct company row links, all linked matters, and all stakeholders with roles in the entity.
GET /api/csp/entities/:entityId/files
Path Parameters
| Parameter | Type | Description |
|---|---|---|
entityId | string | The row ID of the company entity in the Companies system table |
Response
{
"entityId": "cm_row_ent1",
"entityName": "Acme Holdings Ltd",
"direct": [
{
"id": "cm_file_abc123",
"filename": "certificate-of-incorporation.pdf",
"fileSize": 245760,
"mimeType": "application/pdf",
"createdAt": "2026-01-15T10:00:00.000Z"
}
],
"fromMatters": [
{
"matterId": "cm_matter_789",
"matterTitle": "Acme Holdings - Annual Return 2026",
"files": [
{
"id": "cm_file_def456",
"filename": "annual-return-draft.pdf",
"fileSize": 184320,
"mimeType": "application/pdf",
"createdAt": "2026-02-20T14:30:00.000Z"
}
]
}
],
"fromStakeholders": [
{
"individualId": "cm_row_ind1",
"name": "John Smith",
"files": [
{
"id": "cm_file_ghi789",
"filename": "passport-scan.pdf",
"fileSize": 102400,
"mimeType": "application/pdf",
"createdAt": "2026-01-10T09:00:00.000Z"
}
]
}
],
"totalFiles": 3
}
Individual Files
Deep file aggregation for a CSP individual. Returns files from direct stakeholder row links, linked matters, and bridged KYC documents.
GET /api/csp/individuals/:individualId/files
Path Parameters
| Parameter | Type | Description |
|---|---|---|
individualId | string | The row ID of the individual in the Stakeholders system table |
Response
{
"individualId": "cm_row_ind1",
"individualName": "John Smith",
"direct": [
{
"id": "cm_file_ghi789",
"filename": "passport-scan.pdf",
"fileSize": 102400,
"mimeType": "application/pdf",
"createdAt": "2026-01-10T09:00:00.000Z"
}
],
"fromMatters": [
{
"matterId": "cm_matter_789",
"matterTitle": "Acme Holdings - Annual Return 2026",
"files": [
{
"id": "cm_file_jkl012",
"filename": "director-resolution.pdf",
"fileSize": 76800,
"mimeType": "application/pdf",
"createdAt": "2026-02-22T11:00:00.000Z"
}
]
}
],
"totalFiles": 2
}
Stakeholder File Timeline
Chronological view of every document associated with a stakeholder across all entities and matters.
GET /api/stakeholders/:id/files
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | The stakeholder row ID |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
limit | number | Maximum entries to return (default: 100, max: 500) |
Response
{
"stakeholderId": "cm_row_ind1",
"entries": [
{
"file": {
"id": "cm_file_ghi789",
"filename": "passport-scan.pdf",
"fileSize": 102400,
"mimeType": "application/pdf"
},
"source": "ROW",
"sourceLabel": "Stakeholders - John Smith",
"date": "2026-01-10T09:00:00.000Z"
},
{
"file": {
"id": "cm_file_jkl012",
"filename": "director-resolution.pdf",
"fileSize": 76800,
"mimeType": "application/pdf"
},
"source": "MATTER",
"sourceLabel": "Acme Holdings - Annual Return 2026",
"date": "2026-02-22T11:00:00.000Z"
}
],
"totalEntries": 2
}