File Storage API
Files are stored on IPFS and addressed by a CID, the hash of the stored bytes. Two facts about this platform shape the API.
- What is stored is an envelope rather than your file. Every upload is sealed under a fresh AES-256-GCM content key before it reaches IPFS, and that key is wrapped to the service's ML-KEM-1024 key inside the same envelope. The CID names the envelope. Because the key and the nonce are fresh each time, uploading the same file twice yields two different CIDs, each with one owner.
- A CID grants no access. Every read goes through an ownership check or a capability check, and only the API can open the envelope.
Upload
POST /api/upload takes multipart/form-data with one file part. The filename is reduced to a safe basename server side. The content type is taken from the part, or guessed from the name when the part does not carry one.
curl -X POST https://api.ekayana.io/api/upload \
-H "Authorization: Bearer $TOKEN" \
-F "file=@genome.fasta"{
"cid": "bafybeig...",
"name": "genome.fasta",
"size": 1048576,
"content_type": "application/octet-stream",
"timestamp": "2026-07-24T10:00:00Z",
"user_id": 42
}size is the plaintext size. The CID is also returned in an x-cid response header, so a client can read it without parsing the body. The request body is capped at MAX_UPLOAD_BYTES, 100 MiB by default. A larger body answers 413.
Asynchronous upload for large files
Add ?async=true and the call returns at once with a task handle instead of holding the request open until the cluster has the bytes.
curl -X POST "https://api.ekayana.io/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",
"completed_at": null
}Poll GET /api/upload/status/{task_id} until status becomes completed, with cid populated and progress at 100, or failed, with error set. A task is visible only to the account that started it.
Download
curl https://api.ekayana.io/api/download/bafybeig... \
-H "Authorization: Bearer $TOKEN" -o genome.fastaThe response carries the stored content type and a Content-Disposition header with the original name. Access is granted in two ways.
- Ownership. You uploaded the file.
- Delegation. You hold a UCAN pass granting
readon the CID. You present the pass inx-ucan-tokentogether with an invocation your session key signed inx-ucan-invocation.
curl https://api.ekayana.io/api/download/bafybeig... \
-H "Authorization: Bearer $TOKEN" \
-H "x-ucan-token: $UCAN" \
-H "x-ucan-invocation: $INVOCATION"The session token identifies you, the pass carries the grant, and the invocation proves the request is yours. POST /api/ucan/invoke/prepare gives you the bytes to sign for the invocation. The full flow is on Sharing & Delegation.
Whichever way access is granted, the API fetches the envelope from IPFS, checks its authentication tag against the owning account, opens it with the content key and returns the plaintext. Nothing else can open it, because the ML-KEM decapsulation key never leaves the service. See Privacy Controls.
Metadata, listing, deletion
| Operation | Endpoint |
|---|---|
| File metadata | GET /api/metadata/{cid} |
| List your CIDs | GET /api/pins |
| Delete | POST /api/delete with { "cid": "bafybeig..." } |
GET /api/metadata/{cid} returns the same record as the upload response and accepts a read pass the same way download does. GET /api/pins returns a plain array of your CIDs, newest first.
Deletion
POST /api/delete removes your record for the CID, then unpins the envelope from IPFS when no record references it any more. Since every upload has its own CID, that is the normal case, and the reference count is only a safeguard. A holder of a pass granting delete can delete the owner's file the same way, with the pass and invocation headers.
Unpinning releases the platform's copy, and the blocks leave the node's disk when its garbage collector next runs. An envelope already fetched by a third party cannot be recalled, and it cannot be opened either, because its content key is wrapped to a key only the service holds. Every upload is sealed for this reason rather than relying on deletion.