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

POST /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

FieldTypeReqDescription
fileFile*The file to upload (max 50 MB)
taskIdstring (UUID)Link the upload to a task

Response Body

FieldTypeReqDescription
idstring (UUID)*Document ID (also serves as the upload ID)
filenamestring*Stored filename (UUID-based with original extension)
originalNamestring*Original filename from the upload
sizenumber*File size in bytes
typestring*MIME type from the uploaded file
taskIdstring | 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

GET /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

DELETE /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

POST /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

FieldTypeReqDescription
deletednumber*Number of orphaned files removed
errorsnumber*Number of files that failed to delete
skippednumber*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:

ExtensionMIME Type
.txttext/plain
.mdtext/markdown
.jsonapplication/json
.jstext/javascript
.tstext/typescript
.pytext/x-python
.htmltext/html
.csstext/css
.pngimage/png
.jpg / .jpegimage/jpeg
.gifimage/gif
.svgimage/svg+xml
.pdfapplication/pdf

All other extensions are served as application/octet-stream.