Appearance
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:
| Permission | Opens |
|---|---|
upload:files | Reserving, confirming, adding and releasing references |
read:files | The record, the scan history, download URLs and the scanner status |
retain:files | Changing retention and putting a legal hold on |
purge:files | Purging a file by hand |
admin:system | On 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
| Method | Route | Permission | Success |
|---|---|---|---|
POST | /files | upload:files | 201 reservation: fileId, uploadUrl, uploadExpiresAt, requiredHeaders |
POST | /files/{id}/complete | upload:files | 200 the record, now Scanning |
GET | /files/{id} | read:files | 200 the record, in any state — a tombstone included |
GET | /files/{id}/download-url?purpose= | read:files | 200 { url, expiresAt } |
GET | /files/{id}/scan-attempts | read:files | 200 the attempts, newest first |
POST | /files/{id}/references | upload:files | 201 the reference; the same row again for a pair that already holds the file |
DELETE | /files/{id}/references/{referenceId} | upload:files | 204; releasing twice is releasing once |
PUT | /files/{id}/retention | retain:files | 200 the record |
POST | /files/{id}/purge | purge:files | 200 the tombstone |
GET | /files/scanner-status | read:files | 200 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. objectKeyandbucketNamenever 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
AvailablefromInfected, and nothing reachesAvailablewithout a scan.
Errors by route
| Route | Refusals |
|---|---|
POST /files | 400 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}/complete | 404 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-attempts | 404 FILE_NOT_FOUND |
GET /files/{id}/download-url | 404 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}/references | 404 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}/retention | 400 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}/purge | 400 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-status | 503 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:
| Suffix | Status |
|---|---|
_NOT_FOUND | 404 |
_CONFLICT, _ALREADY_EXISTS | 409 |
_UNPROCESSABLE | 422 |
_UNAVAILABLE | 503 |
| no suffix | 400 |
_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:
| Field | Meaning |
|---|---|
engine | The engine and its version, from the daemon's VERSION reply |
databaseVersion | The signature database version |
databaseAgeHours | Age of the database, or null when unknown |
databaseStale | true above 48 hours, or when the age is unknown |
queueDepth | Files waiting for a verdict |
queueOldestItemAgeSeconds | Age of the oldest waiting file, or null when the queue is empty |
See Watching the scan for the alerts built on it.