Skip to content

API Reference ​

Ten routes under /api/v1/files. Each answers JSON — metadata, a URL or a problem — and none of them accepts or returns the content of a file (RN-GF-06). The contract of every field is §15–§17 of the specification; the reasons are in ADR 0010.

Permissions ​

Four permissions, in the framework's action:resource spelling (D1), plus the platform permission that already exists:

PermissionOpens
upload:filesReserving, confirming, adding and releasing references
read:filesThe record, the scan history, download URLs and the scanner status
retain:filesChanging retention and putting a legal hold on
purge:filesPurging a file by hand
admin:systemOn top of the module permission, never instead of it: reserving, changing the retention of and purging a file with no tenant (a product file); purging a Permanent file; releasing a legal hold

admin:system alone opens no route. Reading a product file needs only read:files.

Routes ​

MethodRoutePermissionSuccess
POST/filesupload:files201 reservation: fileId, uploadUrl, uploadExpiresAt, requiredHeaders
POST/files/{id}/completeupload:files200 the record, now Scanning
GET/files/{id}read:files200 the record, in any state — a tombstone included
GET/files/{id}/download-url?purpose=read:files200 { url, expiresAt }
GET/files/{id}/scan-attemptsread:files200 the attempts, newest first
POST/files/{id}/referencesupload:files201 the reference; the same row again for a pair that already holds the file
DELETE/files/{id}/references/{referenceId}upload:files204; releasing twice is releasing once
PUT/files/{id}/retentionretain:files200 the record
POST/files/{id}/purgepurge:files200 the tombstone
GET/files/scanner-statusread:files200 the engine, its database and the queue

What is deliberately absent:

  • There is no DELETE /files/{id}. Destroying content is a purge, with a reason, an author and a permission of its own.
  • objectKey and bucketName never appear in a request, a response, a DTO or a log line (RN-GF-07). The upload and download URLs are the only place a location travels, signed and short-lived.
  • There is no route that changes a verdict. No state goes back to Available from Infected, and nothing reaches Available without a scan.

Errors by route ​

RouteRefusals
POST /files400 FILE_PROFILE_UNKNOWN · 422 FILE_TOO_LARGE_UNPROCESSABLE (the ceiling is in the message) · 422 FILE_CONTENT_TYPE_NOT_ALLOWED_UNPROCESSABLE · 403 for a product file without admin:system
POST /files/{id}/complete404 FILE_NOT_FOUND · 409 FILE_NOT_UPLOADED_CONFLICT · 409 FILE_UPLOAD_EXPIRED_CONFLICT · 422 FILE_SIZE_MISMATCH_UNPROCESSABLE (the object is deleted and the record becomes a tombstone)
GET /files/{id}, GET /files/{id}/scan-attempts404 FILE_NOT_FOUND
GET /files/{id}/download-url404 FILE_NOT_FOUND · 409 FILE_NOT_AVAILABLE_CONFLICT (Pending, Scanning, Failed) · 409 FILE_INFECTED_CONFLICT · 409 FILE_PURGED_CONFLICT (with when and why)
POST /files/{id}/references404 FILE_NOT_FOUND · 409 FILE_NOT_AVAILABLE_CONFLICT · 409 FILE_INFECTED_CONFLICT · 409 FILE_PURGED_CONFLICT
DELETE /files/{id}/references/{referenceId}404 FILE_NOT_FOUND, for the file or for a reference that is not on it
PUT /files/{id}/retention400 incoherent body (no code) · 404 FILE_NOT_FOUND · 409 FILE_PURGED_CONFLICT · 422 FILE_RETENTION_SHORTENED_UNPROCESSABLE · 403 releasing a hold or touching a product file without admin:system
POST /files/{id}/purge400 without a reason, or with a job: reason (D16) · 404 FILE_NOT_FOUND · 409 FILE_STILL_REFERENCED_CONFLICT (the holding scopes in errors) · 409 FILE_LEGAL_HOLD_CONFLICT · 409 for a file that is not Available, with its state's code · 403 without admin:system where it is required
GET /files/scanner-status503 FILE_SCAN_UNAVAILABLE

A file of another tenant is answered exactly as a file that does not exist: 404 FILE_NOT_FOUND with the same body and the same headers, on every route that names a file, whatever the state of the file — never a 409 or a 422 first. The lookup decides before anything else is looked at.

The error contract (§16) ​

Every refusal is a problem document with the code in code and, where the answer is a list, the list in errors. The status follows from the suffix, the same rule the whole framework uses:

SuffixStatus
_NOT_FOUND404
_CONFLICT, _ALREADY_EXISTS409
_UNPROCESSABLE422
_UNAVAILABLE503
no suffix400

_UNAVAILABLE is delivered, not proposed: FILE_SCAN_UNAVAILABLE is answered by GET /files/scanner-status, and only by it (D6). With the engine down, every other route answers as it always does — reservations are accepted and queue up, files wait in Scanning.

rejectionCode — why a file was refused after the upload ​

The content type is checked twice. At the reservation the declared type is checked against the profile, and a type outside the list is 422 FILE_CONTENT_TYPE_NOT_ALLOWED_UNPROCESSABLE before any URL is issued. After the upload, the scan job reads the real type from the content — by signature, by the OOXML package's [Content_Types].xml, or as text with no NUL byte.

That second refusal does not answer 422 (D4): the confirmation already answered 200 before anybody looked at the bytes. The record goes Scanning → Purged, the object is deleted, and the code goes onto the record as rejectionCode, which GET /files/{id} and the FILE_PURGED_CONFLICT of the other routes carry back. The tombstone keeps the sha256 of what was read, and its purgeReason is job:tipo-reprovado. rejectionCode holds a code of the §16 catalogue, never a vocabulary of its own.

Download headers (D5) ​

The URL is followed straight to the bucket, which answers with the two headers the module signed into it:

  • Content-Disposition: attachment; filename*=UTF-8''<original name> — the browser saves, never renders;
  • Content-Type: application/octet-stream — never the detected type, which a browser would display.

X-Content-Type-Options: nosniff is not part of the contract: a presigned URL cannot carry it on any of the three providers. It is a bucket or CDN setting — see Download.

Scanner status ​

GET /files/scanner-status answers the one reading the health check and the alerts also use:

FieldMeaning
engineThe engine and its version, from the daemon's VERSION reply
databaseVersionThe signature database version
databaseAgeHoursAge of the database, or null when unknown
databaseStaletrue above 48 hours, or when the age is unknown
queueDepthFiles waiting for a verdict
queueOldestItemAgeSecondsAge of the oldest waiting file, or null when the queue is empty

See Watching the scan for the alerts built on it.

Released under the MIT License.