Tutorial

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 keyUCAN delegation
ScopeWhole accountOne CID, listed actions
LifetimeUntil revoked by handSigned expiry, always enforced
Who signsThe server, on your behalfYour wallet, and only your wallet
Passing it onShare the secretRedelegate 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

bash
curl "https://api.ekayana.io/api/users/search?q=bob" \
  -H "Authorization: Bearer $TOKEN"
json
{ "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

bash
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
  }'
json
{ "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.

bash
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>"
  }'
json
{
  "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.

bash
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.

bash
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.fasta

The 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

bash
curl https://api.ekayana.io/api/shared -H "Authorization: Bearer $BOB_TOKEN"
json
{
  "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

bash
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.

OperationEndpoint
Prepare a delegationPOST /api/ucan/issue/prepare
Record a signed delegationPOST /api/ucan/issue
Prepare an invocationPOST /api/ucan/invoke/prepare
ValidatePOST /api/ucan/validate (public)
RevokePOST /api/ucan/revoke
Tokens you issuedGET /api/ucan/issued
Tokens you receivedGET /api/ucan/received
Fetch by CIDGET /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.