Uploads API
The Uploads API handles direct file uploads via multipart form data, primarily used by the browser UI. Files are stored in Stagent’s managed uploads directory, a document record is created in the database, and asynchronous processing (text extraction, thumbnails) begins automatically. For programmatic file ingestion from the local filesystem, see the Documents API.
Quick Start
Upload a file, retrieve its metadata, then run a cleanup to remove orphaned files:
// 1. Upload a file via multipart form data
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('taskId', 'task-9d4e-a1b2');
const upload: { id: string; filename: string; originalName: string; size: number } =
await fetch('/api/uploads', {
method: 'POST',
body: formData,
}).then((r) => r.json());
// → { id: "doc-3f8a-...", filename: "3f8a...pdf", originalName: "report.pdf", size: 184320 }
// 2. Download the file by its ID
const downloadUrl: string = `/api/uploads/${upload.id}`;
window.open(downloadUrl); // triggers attachment download
// 3. When done, delete the upload
await fetch(`/api/uploads/${upload.id}`, { method: 'DELETE' });
// → 204 No Content
// 4. Periodically clean up orphaned files (no matching DB record)
const cleanup: { deleted: number; errors: number } = await fetch('/api/uploads/cleanup', {
method: 'POST',
}).then((r) => r.json());
console.log(`Cleaned ${cleanup.deleted} orphans, ${cleanup.errors} errors`); Base URL
/api/uploads
Endpoints
Upload File
/api/uploads Upload a file via multipart/form-data. Creates a document record and triggers asynchronous processing (text extraction, thumbnail generation). Maximum file size is 50 MB.
Form Data Fields
| Field | Type | Req | Description |
|---|---|---|---|
| file | File | * | The file to upload (max 50 MB) |
| taskId | string (UUID) | — | Link the upload to a task |
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| id | string (UUID) | * | Document ID (also serves as the upload ID) |
| filename | string | * | Stored filename (UUID-based with original extension) |
| originalName | string | * | Original filename from the upload |
| size | number | * | File size in bytes |
| type | string | * | MIME type from the uploaded file |
| taskId | string | null | * | Linked task ID if provided |
Response 201 Created
Errors: 400 — No file provided or file exceeds 50 MB
Upload a file from a browser form or programmatically attach it to a task:
// Upload a file from a browser file input
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('taskId', 'task-9d4e-a1b2');
const upload: { id: string; originalName: string; size: number } = await fetch('/api/uploads', {
method: 'POST',
body: formData, // no Content-Type header — browser sets multipart boundary
}).then((r) => r.json());
console.log(`Uploaded: ${upload.originalName} (${upload.size} bytes)`);
console.log(`Document ID: ${upload.id}`); Example response:
{
"id": "doc-3f8a-2b1c",
"filename": "3f8a2b1c-report.pdf",
"originalName": "report.pdf",
"size": 184320,
"type": "application/pdf",
"taskId": "task-9d4e-a1b2"
} Download Upload
/api/uploads/{id} Download an uploaded file by its ID. Files are served as attachment downloads with X-Content-Type-Options: nosniff to prevent stored XSS.
Response 200 — Raw file bytes with Content-Type and Content-Disposition: attachment headers
Security: All files are forced to download (never rendered inline) to prevent stored XSS via attacker-controlled HTML, SVG, or JavaScript content.
Download an uploaded file — the browser will prompt a save dialog because all files are served as attachments:
// Trigger a download in the browser
const link: HTMLAnchorElement = document.createElement('a');
link.href = '/api/uploads/doc-3f8a-2b1c';
link.download = '';
link.click();
// Or fetch the raw bytes for processing
const res: Response = await fetch('/api/uploads/doc-3f8a-2b1c');
const blob: Blob = await res.blob();
console.log(`Downloaded ${blob.size} bytes (${blob.type})`); Errors: 404 — File not found
Delete Upload
/api/uploads/{id} Delete an uploaded file from disk and remove its document record from the database.
Response 204 No Content
Remove an upload when it is no longer needed — deletes both the file on disk and the database record:
// Delete an upload
const res: Response = await fetch('/api/uploads/doc-3f8a-2b1c', { method: 'DELETE' });
if (res.status === 204) {
console.log('Upload deleted successfully');
} else if (res.status === 404) {
console.log('Upload not found — may already be deleted');
} Errors: 404 — File not found, 500 — Deletion failure
Cleanup Orphaned Uploads
/api/uploads/cleanup Scan the uploads directory and remove files that no longer have a matching document record in the database. Returns a summary of cleaned files.
Response Body
| Field | Type | Req | Description |
|---|---|---|---|
| deleted | number | * | Number of orphaned files removed |
| errors | number | * | Number of files that failed to delete |
| skipped | number | * | Number of files still linked to documents |
Run cleanup periodically to reclaim disk space from orphaned files — files whose database records were deleted but the physical file remained:
// Run orphan cleanup and log results
const result: { deleted: number; errors: number; skipped: number } = await fetch(
'/api/uploads/cleanup',
{ method: 'POST' }
).then((r) => r.json());
console.log(`Deleted: ${result.deleted} orphaned files`);
console.log(`Errors: ${result.errors} files failed to delete`);
console.log(`Skipped: ${result.skipped} files still linked`); Example response:
{
"deleted": 7,
"errors": 0,
"skipped": 43
} Supported MIME Types
The following types are detected by file extension for the Content-Type response header:
| Extension | MIME Type |
|---|---|
.txt | text/plain |
.md | text/markdown |
.json | application/json |
.js | text/javascript |
.ts | text/typescript |
.py | text/x-python |
.html | text/html |
.css | text/css |
.png | image/png |
.jpg / .jpeg | image/jpeg |
.gif | image/gif |
.svg | image/svg+xml |
.pdf | application/pdf |
All other extensions are served as application/octet-stream.