Sharing & Delegation
Sharing a file does not copy it or make it public. Your wallet signs a UCAN delegation that names one CID, one recipient, a set of actions and an expiry. The recipient presents it with an invocation their own key signed, and the service checks the whole chain before serving a byte. Any share can be revoked.
Why there are no API keys
The platform has no API keys. A key is a bearer secret that grants everything its owner can do, until someone rotates it. A delegation differs on every axis.
| API key | UCAN delegation | |
|---|---|---|
| Scope | Whole account | One CID, listed actions |
| Lifetime | Until revoked by hand | Signed expiry, always enforced |
| Who signs | The server, on your behalf | Your wallet, and only your wallet |
| Passing it on | Share the secret | Redelegate a subset, and the chain is verified |
If a delegation leaks, the exposure is one file, for the actions it names, until it expires.
1. Find the recipient
curl "https://api.ekayana.io/api/users/search?q=bob" \
-H "Authorization: Bearer $TOKEN"{ "users": [{ "id": 7, "username": "bob", "did": "did:bio:devnet:4Nq8..." }], "total": 1 }The did is the recipient's wallet DID. A user without a linked wallet has no DID and cannot receive a share yet.
2. Prepare the share
curl -X POST https://api.ekayana.io/api/share/file/prepare \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cid": "bafybeig...",
"recipient_did": "did:bio:devnet:4Nq8...",
"capabilities": ["read"],
"expiration_days": 7
}'{ "signing_bytes": "<base64url>", "expires_at": 1753833600 }You can only share files you own, and not with yourself. capabilities is read, write, delete or *. expiration_days runs from 1 to 3650. The bytes returned are the exact DAG-CBOR payload of the delegation, so what the wallet signs is what will be recorded.
3. Sign it with your wallet
The app asks the wallet to sign signing_bytes. This is a plain message signature rather than a transaction. There is no fee and nothing is sent to the chain.
4. Finish the share
Post the same request again, with the bytes and the signature.
curl -X POST https://api.ekayana.io/api/share/file \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cid": "bafybeig...",
"recipient_did": "did:bio:devnet:4Nq8...",
"capabilities": ["read"],
"expiration_days": 7,
"signing_bytes": "<base64url>",
"signature": "<base64url>"
}'{
"token": "<base64url of the DAG-CBOR bytes>",
"token_hash": "bafyrei...",
"expires_at": 1753833600,
"cid": "bafybeig...",
"recipient_did": "did:bio:devnet:4Nq8...",
"capabilities": ["read"]
}The service assembles the token, checks that it says what you asked and verifies under your wallet's key, then records it. Send token to the recipient. token_hash is the token's CID, and the handle you revoke with.
5. Redeem it
The recipient never presents a bare delegation. They present it together with an invocation signed by a key of theirs. For reads that key is the browser session key their wallet delegated on sign in. The app handles both steps. The calls it makes are below.
Once per session, register a browser key.
curl -X POST https://api.ekayana.io/api/ucan/session/prepare \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "session_did": "did:key:z6Mk..." }'
# the wallet signs, then
curl -X POST https://api.ekayana.io/api/ucan/session \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "session_did": "did:key:z6Mk...", "signing_bytes": "...", "signature": "..." }'That grant lets the browser key invoke /file/read for 24 hours and nothing wider. For each read, prepare an invocation, sign it with the session key, and download.
curl -X POST https://api.ekayana.io/api/ucan/invoke/prepare \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "token": "<delegation>", "cid": "bafybeig...", "action": "read" }'
# -> { "signing_bytes": "...", "expires_at": ... }, valid for 60 seconds
curl https://api.ekayana.io/api/download/bafybeig... \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "x-ucan-token: <delegation>" \
-H "x-ucan-invocation: <invocation>" -o shared.fastaThe service checks the delegation's signature, time window and revocation, confirms the invocation names this CID and action, and runs the validator over the chain with itself as executor. Only then does it decrypt.
6. See what was shared with you
curl https://api.ekayana.io/api/shared -H "Authorization: Bearer $BOB_TOKEN"{
"files": [{
"cid": "bafybeig...", "name": "genome.fasta", "size": 1048576,
"shared_by": "did:bio:devnet:HP3Hj9en...", "capabilities": ["read"],
"expires_at": 1753833600, "token_hash": "bafyrei..."
}],
"total": 1
}Expired and revoked grants are filtered out server side.
7. Revoke
curl -X DELETE https://api.ekayana.io/api/share/revoke/bafyrei... \
-H "Authorization: Bearer $TOKEN"Revocation takes effect on the next presentation. There is no cached allow list to wait on. Only the issuer may revoke. Anything delegated beneath a revoked token fails with it, because its proof chain no longer resolves.
The general API underneath
The share endpoints wrap the UCAN API, which you can use directly for grants that are not about one file.
| Operation | Endpoint |
|---|---|
| Prepare a delegation | POST /api/ucan/issue/prepare |
| Record a signed delegation | POST /api/ucan/issue |
| Prepare an invocation | POST /api/ucan/invoke/prepare |
| Validate | POST /api/ucan/validate (public) |
| Revoke | POST /api/ucan/revoke |
| Tokens you issued | GET /api/ucan/issued |
| Tokens you received | GET /api/ucan/received |
| Fetch by CID | GET /api/ucan/token/{cid} |
/api/ucan/validate is public so that a verifier can check a token without holding an account. See UCAN Authorization for the token format, the policy language and the validation rules.