File Storage API
Files are stored on IPFS and addressed by CID - a hash of the content itself. Two consequences shape this whole API:
- Identical bytes produce an identical CID. Uploading the same file twice yields one CID with two owners, which is why deletion is reference-counted.
- A CID is not a permission. Knowing a CID does not grant access; every read goes through an ownership or capability check.
Upload
POST /api/upload takes multipart/form-data. Filenames are sanitised server-side.
curl -X POST https://api.ekayana.com/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@genome.fasta"{
"cid": "QmXyz…",
"name": "genome.fasta",
"size": 1048576,
"timestamp": "2026-07-24T10:00:00Z",
"user_id": 42
}The CID is also returned in an X-CID response header, so a client can read it without parsing the body.
Large files: upload asynchronously
Add ?async=true and the call returns immediately with a task handle instead of blocking until IPFS has the bytes.
curl -X POST "https://api.ekayana.com/api/upload?async=true" \
-H "Authorization: Bearer $TOKEN" \
-F "file=@wgs-run.tar"{
"task_id": "3f9c…",
"status": "pending",
"cid": null,
"error": null,
"progress": 0.0,
"started_at": "2026-07-24T10:00:00Z"
}Poll GET /api/upload/status/{task_id} until status becomes completed (with cid populated) or failed (with error).
Concurrency is bounded server-side by MAX_CONCURRENT_UPLOADS (default 50); work beyond that queues rather than being rejected.
Download
curl https://api.ekayana.com/api/download/QmXyz… \
-H "Authorization: Bearer $TOKEN" -o genome.fastaAccess is granted two ways:
- Ownership - you uploaded it.
- Delegation - you hold a UCAN granting
readonipfs://{cid}, presented in an additional header:
curl https://api.ekayana.com/api/download/QmXyz… \
-H "Authorization: Bearer $TOKEN" \
-H "X-UCAN-Token: $UCAN"The session token still identifies you; the UCAN carries the grant. See Sharing & Delegation.
Payloads written through the encrypted path are sealed with AES-256-GCM under an ML-KEM-wrapped content key and are transparently decrypted on read - a legacy plaintext object is detected and passed through unchanged, so both coexist. See Privacy Controls.
Metadata, listing, deletion
| Operation | Endpoint |
|---|---|
| File metadata | GET /api/metadata/{cid} |
| List your pinned CIDs | GET /api/pins |
| Delete | POST /api/delete with { "cid": "Qm…" } |
GET /api/metadata/{cid} accepts X-UCAN-Token for delegated reads, same as download.
Deletion is reference-counted
POST /api/delete removes your metadata row, then unpins from IPFS only if no other user still holds that CID. This is a direct consequence of content addressing: your upload and a colleague's identical upload are the same object, so one party deleting must not destroy the other's data.
GDPR note: unpinning removes the platform's copy. Content already fetched by third parties cannot be recalled - which is exactly why personal data is encrypted before it reaches IPFS rather than relying on deletion.