FileNest/Docs

Files API

See Upload Flow & Processing for a full walkthrough of how uploads, presigned URLs, and the processing pipeline work together.

Initiate upload

POST /v1/projects/{project_id}/files/upload

Required scope: files:upload
Content-Type: application/json

{
  "filename": "invoice.pdf",
  "content_type": "application/pdf",
  "size_bytes": 204800,
  "folder_id": null,
  "metadata": { "invoiceId": "INV-0042" }
}

Response 201:

{
  "file_id": "file_01j...",
  "upload_url": "https://s3.amazonaws.com/...",
  "expires_at": "2026-06-21T11:00:00Z"
}

PUT the file bytes to upload_url (no Authorization header needed — the signature is embedded in the URL). Then call /confirm to trigger the pipeline.

Confirm upload

POST /v1/projects/{project_id}/files/{file_id}/confirm

Required scope: files:upload

Call this immediately after the PUT to upload_url succeeds. This is the pipeline trigger — it transitions the file out of uploading and starts virus scan / MIME validation if enabled.

Response 200:

{
  "id": "file_01j...",
  "status": "processing"
}

status will be "processing" when virus_scan_enabled is on, or "ready" immediately if not.

List files

GET /v1/projects/{project_id}/files

Required scope: files:read

Query parameters:

ParamTypeDescription
folder_idstringFilter to a specific folder
qstringCase-insensitive filename search (ILIKE %q%)
tagsstring (repeatable)Return files containing all specified tags — ?tags=a&tags=b
categorystringFilter by classification category (e.g. document, image, video)
statusstringFilter by lifecycle status
date_fromstringISO 8601 — earliest created_at to include
date_tostringISO 8601 — latest created_at to include
size_minnumberMinimum file size in bytes
size_maxnumberMaximum file size in bytes
metadatastringJSON object — returns files whose metadata contains all key-value pairs (JSONB containment)
limitnumberPage size (default 50, max 200)
offsetnumberZero-based page offset for table pagination
cursorstringCursor from next_cursor for keyset (infinite-scroll) pagination. Takes priority over offset.

Response 200:

{
  "items": [ /* file objects */ ],
  "total": 142,
  "limit": 50,
  "offset": 0,
  "has_more": true,
  "next_cursor": "file_01j..."
}

Use cursor / next_cursor for infinite scroll; use offset for page-number tables. total is always the full un-paginated count matching the current filters.

Get a file

GET /v1/projects/{project_id}/files/{file_id}

Required scope: files:read

Download a file

GET /v1/projects/{project_id}/files/{file_id}/download

Required scope: files:download

Returns a presigned download URL. The TTL defaults to the project's signed_url_ttl_seconds value. When require_signed_urls is enabled, the caller cannot override the TTL.

Query parameters:

ParamTypeDescription
ttlnumberURL lifetime in seconds (60–86 400). Ignored when require_signed_urls is on.

Response 200:

{
  "url": "https://s3.amazonaws.com/...",
  "expires_at": "2026-06-21T11:00:00Z"
}

Tags

See Metadata & Tags API for the full reference. Quick summary:

PUT  /v1/projects/{project_id}/files/{file_id}/tags   # replace full tag set
POST /v1/projects/{project_id}/files/{file_id}/tags   # add tags (union)

Both require scope files:update_metadata.

Metadata

PUT  /v1/projects/{project_id}/files/{file_id}/metadata   # replace metadata object
POST /v1/projects/{project_id}/files/{file_id}/metadata   # merge metadata (shallow)

Both require scope files:update_metadata. If the project has enforce_schema = true and an active metadata schema, the payload is validated and a 422 is returned on mismatch.

See Metadata & Tags API for schema management and filtering.

Move to folder

POST /v1/projects/{project_id}/files/{file_id}/move

Required scope: files:update_metadata

{ "folder_id": "fld_01j..." }

Set folder_id to null to move the file back to the project root. See Folders API.

Delete a file

DELETE /v1/projects/{project_id}/files/{file_id}

Required scope: files:delete

Soft-deletes the file by setting deleted_at. The storage object is removed by a background job. Files under a legal hold cannot be deleted (Phase 8).


File versions

List versions

GET /v1/projects/{project_id}/files/{file_id}/versions

Required scope: files:read

Returns all version snapshots, newest first. Requires versioning_enabled = true on the project.

Download a version

GET /v1/projects/{project_id}/files/{file_id}/versions/{version_id}/download

Required scope: files:download

Same TTL rules as the main download endpoint.

Restore a version

POST /v1/projects/{project_id}/files/{file_id}/versions/{version_id}/restore

Required scope: files:update_metadata

Sets the file's current storage_key, size_bytes, and content_type to match the chosen version. Creates a new version row capturing the restore event.


Multipart upload

Use multipart for files over 100 MB or when you need to resume an interrupted upload.

Start

POST /v1/projects/{project_id}/files/upload/multipart/start

Required scope: files:upload

{
  "filename": "recording.mp4",
  "content_type": "video/mp4",
  "total_size_bytes": 1073741824,
  "folder_id": null,
  "metadata": {}
}

Response 201:

{ "upload_id": "session_01j...", "file_id": "file_01j..." }

Get a part URL

GET /v1/projects/{project_id}/files/upload/multipart/{upload_id}/part-url

Required scope: files:upload

Query parameters:

ParamTypeDescription
partnumber1-based part number (max 10 000)
ttlnumberURL TTL in seconds. Ignored when require_signed_urls is on.

Response 200:

{
  "upload_id": "session_01j...",
  "part_number": 1,
  "url": "https://s3.amazonaws.com/...",
  "expires_at": "2026-06-21T11:00:00Z"
}

PUT the bytes for that part to url. Save the ETag from the response — you need it in complete.

Complete

POST /v1/projects/{project_id}/files/upload/multipart/{upload_id}/complete

Required scope: files:upload

{
  "parts": [
    { "part_number": 1, "etag": "\"d41d8cd9...\"" },
    { "part_number": 2, "etag": "\"b6d81b36...\"" }
  ]
}

Assembles all parts into the final object and triggers the processing pipeline (same as confirm for single-file uploads).

Abort

DELETE /v1/projects/{project_id}/files/upload/multipart/{upload_id}

Required scope: files:upload

Instructs S3 to discard all uploaded parts and marks the file failed.


Error codes

StatusCodeMeaning
403PERMISSION_DENIEDIP not in project allowlist
404NOT_FOUNDFile does not exist or is deleted
409CONFLICTFile quarantined — virus detected
413FILE_TOO_LARGEFile exceeds the project's max upload size
422VALIDATION_ERRORContent type or extension not allowed by project config