Files API
Files are content-addressed by SHA-256 hash. The same bytes always produce the same hash, so identical files are stored only once. Upload files before pushing a version that references them.
Workflow
- Compute the SHA-256 hash of your file locally
- Check if it exists with
HEAD - If not, upload it with
PUT - Reference it in records as
{"$file": "sha256:<hash>"} - Push your version. The server verifies all referenced files exist
HEAD /api/collections/:owner/:slug/files/:hash
No auth required
Check if a file exists. Returns headers only, no body.
Parameters
:hash | SHA-256 hash, optionally prefixed with sha256: |
Response
200 | File exists. Content-Length and Content-Type headers set. |
404 | File not found. |
Example
curl -I https://underlay.org/api/collections/kf/archive/files/sha256:a1b2c3...
# HTTP/2 200
# Content-Length: 1048576
# Content-Type: application/pdfGET /api/collections/:owner/:slug/files/:hash
Access follows the collection’s visibility
Download a file. After an access check in the context of this collection, the endpoint 302-redirects to a short-lived, presigned storage URL — follow the redirect (e.g. curl -L) to fetch the bytes. Public-collection files are readable anonymously; private content requires access (a session, or a share/agent token sent as a Bearer header). Inaccessible files return 404.
This API path is the durable, content-addressed locator — the same request against a mirror returns the same bytes for the same hash. The redirect target is ephemeral and must not be persisted or shared; always re-fetch through the API path. To resolve many files in one request, see the bulk-presign endpoint below.
Example
# -L follows the 302 redirect to the short-lived presigned URL
curl -L -o paper.pdf \
https://underlay.org/api/collections/kf/archive/files/sha256:a1b2c3...POST /api/collections/:owner/:slug/files/presign
Access follows the collection’s visibility
Presign a batch of files in one request. Body: { "hashes": ["sha256:…", …] }. Returns an object mapping each hash to a short-lived presigned URL, or null when the file is not accessible. Same access model as the single download; avoids one round-trip per file.
PUT /api/collections/:owner/:slug/files/:hash
Auth: write scope
Upload a file. The server verifies the SHA-256 hash of the uploaded bytes matches the hash in the URL. If the file already exists, returns 200 without re-uploading.
Request
Send the file as the raw request body with the appropriate Content-Type header.
# Compute hash
HASH=$(shasum -a 256 paper.pdf | cut -d' ' -f1)
# Upload
curl -X PUT \
"https://underlay.org/api/collections/kf/archive/files/sha256:$HASH" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/pdf" \
--data-binary @paper.pdfResponse 201
{
"hash": "a1b2c3d4e5f6...",
"size": 1048576
}Errors
200 | File already exists: {"hash": "...", "status": "exists"} |
400 | Hash mismatch: the uploaded bytes don't match the hash in the URL. |
File references in records
To link a file to a record, use the $file convention:
{
"id": "pub-001",
"type": "Publication",
"data": {
"title": "An Example Paper",
"pdf": {"$file": "sha256:a1b2c3d4e5f6..."},
"thumbnail": {"$file": "sha256:f6e5d4c3b2a1..."}
}
}When pushing a version, the server scans all record data for $file references and verifies each referenced file exists. If any are missing, the push returns 422 with a list of needed hashes.