Skip to main content

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.

note

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint tables below.


Endpoints

EndpointAuthDescription
POST /files/uploadUpload a file (multipart/form-data)
POST /files/presignGet presigned upload URL (direct browser → S3)
GET /files/:idDownload file or redirect to signed URL
GET /files/:id/urlGet presigned download URL
GET /filesList files (cursor-paginated)
DELETE /files/:idSoft delete — move to trash (30-day restore window)
DELETE /files/:id/permanentPermanent delete — irreversible, frees S3 immediately
POST /files/:id/restoreRestore from trash (within 30 days)
POST /files/:id/processProcess image (resize / crop / convert / compress)
GET /files/:id/thumbnail/:presetGet 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 stateAccess via GET /files/:id
pending_scan404 — file not yet in main bucket
available200 — download or redirect
rejected422 — scan failed, file permanently removed

Staging overflow protection:

LimitValueEnv var
Staging file TTL24 hours — files older than 24 h are auto-rejectedFILES_STAGING_TTL_HOURS
Max staged files per tenant100 — exceeded → 503FILES_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"
}

url is null while status = pending_scan. Poll GET /files/:id or subscribe to the files.file.uploaded event; url will be populated once status = available.

Request Fields

FieldRequiredDescription
fileBinary file content (multipart part)
bucketStorage category: avatars, assets, documents, exports, modules
originalNameOriginal filename (preserved, used in download headers)
mimeTypeMIME type override — auto-detected if omitted
metaArbitrary 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

ParameterValueEnv var
URL TTL15 minutesFILES_PRESIGN_TTL_MIN
Max file size (images)10 MBFILES_MAX_IMAGE_SIZE_MB
Max file size (documents)50 MBFILES_MAX_DOCUMENT_SIZE_MB
Exceeded size413 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"
}
ParameterDefaultMax
ttlSeconds300 (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 paramDescription
bucketFilter by bucket: avatars, assets, documents, exports, modules
statusFilter by status: pending_scan, available, rejected, deleted
uploadedByFilter by uploader user ID
limitPage size (max 100, default 20)
cursorOpaque 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.

PolicyValueEnv var
Restore window30 daysFILES_SOFT_DELETE_RETENTION_DAYS
After 30 daysAuto permanent delete from S3
Storage billingTenant 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 as files.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 /restore returns 410 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

ParameterValueEnv var
Max input image size10 MBFILES_MAX_IMAGE_SIZE_MB
Processing timeout30 secondsFILES_IMAGE_PROCESSING_TIMEOUT_SEC
Timeout behaviourOriginal saved; thumbnails = pending (background retry)
Output quality1–100, default 85
Supported output formatswebp, avif, png, jpeg

Supported Operations

OperationFieldsDescription
cropx, y, width, heightCrop to rectangle (pixels)
resizewidth, height (one optional)Scale; preserves aspect ratio if one dimension omitted
convertformatRe-encode to webp / avif / png / jpeg
rotatedegrees90, 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)

PresetSizeAuto-generatedUse case
icon_3232×32Icons, favicon, sidebar logo
avatar_6464×64☐ (on request or module config)User avatars
card_300300×200Card previews, table rows
preview_600600×400Modal full-size previews
full_12001200×autoFull-width images

If the requested preset has not been generated yet, the service generates it on-demand and caches for future requests.


Bucket Reference

BucketS3 prefixAccess
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 tenantId from 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 typeHot storageAction at expiry
Orphaned (no FK references)30 daysPermanent 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 typeStatusTrigger
problems/file-not-found404File ID does not exist or is in pending_scan
problems/file-rejected422AV scan failed — file infected
problems/file-not-deleted400Restore attempted on a non-deleted file
problems/file-restore-expired41030-day restore window has passed
problems/staging-limit-exceeded503Tenant staging max count reached
problems/payload-too-large413File exceeds size limit
problems/bucket-invalid400Unknown bucket name
problems/processing-failed422Image processing error (unsupported format or corrupt)
problems/preset-not-found404Unknown thumbnail preset name