Files REST API Reference
The Files Service is the central storage hub for avatars, creatives, PWA assets, documents, and module exports. It uses a staging-bucket antivirus pipeline — every upload lands in a private staging bucket first, is anti-virus scanned, then moved to the main bucket when clean.
See the REST API Overview for authentication, error format, pagination, and rate limiting.
For layout brevity, the /api/v1 base path prefix is omitted from the
endpoint tables below.
Endpoints
| Endpoint | Auth | Description |
|---|---|---|
POST /files/upload | ✅ | Upload a file (multipart/form-data) |
POST /files/presign | ✅ | Get presigned upload URL (direct browser → S3) |
GET /files/:id | ✅ | Download file or redirect to signed URL |
GET /files/:id/url | ✅ | Get presigned download URL |
GET /files | ✅ | List files (cursor-paginated) |
DELETE /files/:id | ✅ | Soft delete — move to trash (30-day restore window) |
DELETE /files/:id/permanent | ✅ | Permanent delete — irreversible, frees S3 immediately |
POST /files/:id/restore | ✅ | Restore from trash (within 30 days) |
POST /files/:id/process | ✅ | Process image (resize / crop / convert / compress) |
GET /files/:id/thumbnail/:preset | ✅ | Get thumbnail by preset |
Staging-Bucket Antivirus Pipeline
Every uploaded file passes through a two-bucket scan before it is accessible:
[Client] ──POST /files/upload──► [staging/{tenantId}/{fileId}]
│
S3 Event → File Service
│
AV scan
┌────────────┴────────────┐
Clean Infected
│ │
move to [main bucket] delete from staging
status: available notify tenant-admin
| Lifecycle state | Access via GET /files/:id |
|---|---|
pending_scan | 404 — file not yet in main bucket |
available | 200 — download or redirect |
rejected | 422 — scan failed, file permanently removed |
Staging overflow protection:
| Limit | Value | Env var |
|---|---|---|
| Staging file TTL | 24 hours — files older than 24 h are auto-rejected | FILES_STAGING_TTL_HOURS |
| Max staged files per tenant | 100 — exceeded → 503 | FILES_STAGING_MAX_PER_TENANT |
| AV scan queue backlog | > 1000: newest-first priority inversion | — |
POST /api/v1/files/upload — Multipart Upload
POST https://api.septemcore.com/v1/files/upload
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
file=<binary>
bucket=documents
originalName=report-q1-2026.pdf
mimeType=application/pdf
meta={"source":"crm-module"}
Response 201 Created:
{
"id": "01j9pfil0000000000000001",
"originalName": "report-q1-2026.pdf",
"mimeType": "application/pdf",
"size": 204800,
"bucket": "documents",
"status": "pending_scan",
"url": null,
"uploadedBy": "01j9pusr0000000000000001",
"tenantId": "01j9ptnt0000000000000001",
"createdAt": "2026-04-22T04:10:00Z"
}
urlisnullwhilestatus = pending_scan. PollGET /files/:idor subscribe to thefiles.file.uploadedevent;urlwill be populated oncestatus = available.
Request Fields
| Field | Required | Description |
|---|---|---|
file | ✅ | Binary file content (multipart part) |
bucket | ✅ | Storage category: avatars, assets, documents, exports, modules |
originalName | ✅ | Original filename (preserved, used in download headers) |
mimeType | ☐ | MIME type override — auto-detected if omitted |
meta | ☐ | Arbitrary JSON metadata (max 4 KB) stored with the file record |
POST /api/v1/files/presign — Presigned Upload URL
For large files or browser-direct uploads. Client GETs a presigned S3 URL and PUTs the file directly to the staging bucket — bypassing the platform servers.
POST https://api.septemcore.com/v1/files/presign
Authorization: Bearer <access_token>
Content-Type: application/json
{
"bucket": "assets",
"originalName": "hero-banner.png",
"mimeType": "image/png",
"sizeBytes": 512000
}
Response 200 OK:
{
"uploadUrl": "https://s3.us-east-1.amazonaws.com/platform-staging/...",
"fileId": "01j9pfil0000000000000002",
"expiresAt": "2026-04-22T04:25:00Z",
"fields": {
"Content-Type": "image/png",
"key": "tenant_abc/staging/01j9pfil0000000000000002"
}
}
Client PUTs to uploadUrl then the platform AV pipeline runs automatically.
Presign Limits
| Parameter | Value | Env var |
|---|---|---|
| URL TTL | 15 minutes | FILES_PRESIGN_TTL_MIN |
| Max file size (images) | 10 MB | FILES_MAX_IMAGE_SIZE_MB |
| Max file size (documents) | 50 MB | FILES_MAX_DOCUMENT_SIZE_MB |
| Exceeded size | 413 Payload Too Large | — |
GET /api/v1/files/:id — Download / Redirect
Returns the file as an octet-stream, or a 302 Redirect to a
short-lived presigned S3 URL (default behaviour for most clients):
GET https://api.septemcore.com/v1/files/01j9pfil0000000000000001
Authorization: Bearer <access_token>
Response 302 Found (redirect to presigned S3 URL, default):
Location: https://s3.us-east-1.amazonaws.com/platform-main/...?X-Amz-Expires=300
Or 200 OK with Content-Disposition: attachment when ?download=1 is
passed.
File object response body (with ?meta=1):
{
"id": "01j9pfil0000000000000001",
"originalName": "report-q1-2026.pdf",
"mimeType": "application/pdf",
"size": 204800,
"bucket": "documents",
"status": "available",
"url": "https://cdn.septemcore.com/...",
"thumbnails": {
"icon_32": "https://cdn.septemcore.com/.../icon_32.webp",
"card_300": "https://cdn.septemcore.com/.../card_300.webp"
},
"uploadedBy": "01j9pusr0000000000000001",
"tenantId": "01j9ptnt0000000000000001",
"createdAt": "2026-04-22T04:10:00Z",
"deletedAt": null
}
GET /api/v1/files/:id/url — Presigned Download URL
Returns a time-limited presigned URL without performing a redirect:
GET https://api.septemcore.com/v1/files/01j9pfil0000000000000001/url
?ttlSeconds=3600
Authorization: Bearer <access_token>
Response 200 OK:
{
"url": "https://s3.us-east-1.amazonaws.com/platform-main/...?X-Amz-Expires=3600",
"expiresAt": "2026-04-22T05:10:00Z"
}
| Parameter | Default | Max |
|---|---|---|
ttlSeconds | 300 (5 min) | 86400 (24 h) |
GET /api/v1/files — List Files
Cursor-paginated list of files for the current tenant:
GET https://api.septemcore.com/v1/files
?bucket=documents
&status=available
&limit=20
&cursor=<opaque_cursor>
Authorization: Bearer <access_token>
| Query param | Description |
|---|---|
bucket | Filter by bucket: avatars, assets, documents, exports, modules |
status | Filter by status: pending_scan, available, rejected, deleted |
uploadedBy | Filter by uploader user ID |
limit | Page size (max 100, default 20) |
cursor | Opaque pagination cursor from previous response |
DELETE /api/v1/files/:id — Soft Delete
Moves the file to trash. The file is not deleted from S3 immediately. The tenant continues to pay for storage during the retention period.
DELETE https://api.septemcore.com/v1/files/01j9pfil0000000000000001
Authorization: Bearer <access_token>
Response 204 No Content.
| Policy | Value | Env var |
|---|---|---|
| Restore window | 30 days | FILES_SOFT_DELETE_RETENTION_DAYS |
| After 30 days | Auto permanent delete from S3 | — |
| Storage billing | Tenant billed during trash period | — |
DELETE /api/v1/files/:id/permanent — Permanent Delete
Irreversible. The file is deleted from S3 immediately. Storage is freed
right away and Billing recalculates storage_bytes:
DELETE https://api.septemcore.com/v1/files/01j9pfil0000000000000001/permanent
Authorization: Bearer <access_token>
Response 204 No Content.
This endpoint also works on files currently in trash (
status = deleted). There is no undo — Audit records the action asfiles.file.permanent_deleted.
POST /api/v1/files/:id/restore — Restore from Trash
POST https://api.septemcore.com/v1/files/01j9pfil0000000000000001/restore
Authorization: Bearer <access_token>
Response 200 OK — returns the full file object with status: available.
Restore is only possible within 30 days of soft deletion. After 30 days the file is permanently deleted and
POST /restorereturns410 Gone.
POST /api/v1/files/:id/process — Image Processing
Triggers bimg (libvips) on the server to resize, crop, convert, or compress
an image:
POST https://api.septemcore.com/v1/files/01j9pfil0000000000000002/process
Authorization: Bearer <access_token>
Content-Type: application/json
{
"operations": [
{ "type": "crop", "x": 10, "y": 20, "width": 800, "height": 600 },
{ "type": "resize", "width": 400 },
{ "type": "convert", "format": "webp" },
{ "type": "rotate", "degrees": 90 }
],
"quality": 85
}
Response 202 Accepted (processing is async):
{
"fileId": "01j9pfil0000000000000002",
"jobId": "01j9pjob0000000000000010",
"status": "processing",
"startedAt": "2026-04-22T04:11:00Z"
}
Poll GET /files/:id — when processing is complete, url is updated and
thumbnails are populated.
Processing Limits
| Parameter | Value | Env var |
|---|---|---|
| Max input image size | 10 MB | FILES_MAX_IMAGE_SIZE_MB |
| Processing timeout | 30 seconds | FILES_IMAGE_PROCESSING_TIMEOUT_SEC |
| Timeout behaviour | Original saved; thumbnails = pending (background retry) | — |
| Output quality | 1–100, default 85 | — |
| Supported output formats | webp, avif, png, jpeg | — |
Supported Operations
| Operation | Fields | Description |
|---|---|---|
crop | x, y, width, height | Crop to rectangle (pixels) |
resize | width, height (one optional) | Scale; preserves aspect ratio if one dimension omitted |
convert | format | Re-encode to webp / avif / png / jpeg |
rotate | degrees | 90, 180, 270 |
GET /api/v1/files/:id/thumbnail/:preset — Get Thumbnail
GET https://api.septemcore.com/v1/files/01j9pfil0000000000000002/thumbnail/card_300
Authorization: Bearer <access_token>
Response 302 Found — redirect to presigned thumbnail URL.
Thumbnail Presets (auto-generated on upload)
| Preset | Size | Auto-generated | Use case |
|---|---|---|---|
icon_32 | 32×32 | ✅ | Icons, favicon, sidebar logo |
avatar_64 | 64×64 | ☐ (on request or module config) | User avatars |
card_300 | 300×200 | ✅ | Card previews, table rows |
preview_600 | 600×400 | ☐ | Modal full-size previews |
full_1200 | 1200×auto | ☐ | Full-width images |
If the requested preset has not been generated yet, the service generates it on-demand and caches for future requests.
Bucket Reference
| Bucket | S3 prefix | Access |
|---|---|---|
avatars | {tenantId}/avatars/ | All modules of the tenant |
assets | {tenantId}/assets/ | All modules of the tenant |
documents | {tenantId}/documents/ | All modules of the tenant |
exports | {tenantId}/exports/ | All modules of the tenant |
modules | {tenantId}/modules/ | All modules of the tenant |
Cross-tenant access is impossible — Gateway enforces
tenantIdfrom JWT before forwarding to the File Service.
Orphaned Files Policy
A file is marked orphaned only when it has zero FK references AND was
created more than 24 hours ago (FILES_ORPHAN_MIN_AGE_HOURS=24) — the
24-hour grace prevents false positives for files uploaded before the module
creates its FK record.
| File type | Hot storage | Action at expiry |
|---|---|---|
| Orphaned (no FK references) | 30 days | Permanent S3 delete + files.orphan.deleted audit record |
| Soft-deleted (in trash) | 30 days (FILES_SOFT_DELETE_RETENTION_DAYS) | Permanent S3 delete |
Background job scans daily (FILES_ORPHAN_SCAN_INTERVAL_HOURS=24):
S3 metadata → PostgreSQL FK check → mark orphaned → delete after 30 days.
Error Reference
| Error type | Status | Trigger |
|---|---|---|
problems/file-not-found | 404 | File ID does not exist or is in pending_scan |
problems/file-rejected | 422 | AV scan failed — file infected |
problems/file-not-deleted | 400 | Restore attempted on a non-deleted file |
problems/file-restore-expired | 410 | 30-day restore window has passed |
problems/staging-limit-exceeded | 503 | Tenant staging max count reached |
problems/payload-too-large | 413 | File exceeds size limit |
problems/bucket-invalid | 400 | Unknown bucket name |
problems/processing-failed | 422 | Image processing error (unsupported format or corrupt) |
problems/preset-not-found | 404 | Unknown thumbnail preset name |