Skip to content

Upload & File Lifecycle

Every file in OpenTusk follows a storage pipeline — from upload, through a fast hot cache, to durable decentralized storage on the Walrus network. This guide covers the full journey: uploading files, paying for storage (PPU), encryption, status transitions, rehydration, and webhook notifications.

Terminal window
# Upload a single file
opentusk upload report.pdf --vault "My Vault"
# Upload multiple files
opentusk upload file1.txt file2.txt --vault "My Vault"
# Upload a directory recursively
opentusk upload ./docs --vault "My Vault" --recursive
# Upload to a specific folder within a vault
opentusk upload invoice.pdf --vault "My Vault" --folder <folder-id>
# Upload inline text content (no file on disk needed)
opentusk upload --content "Hello world" --name notes.txt --vault "My Vault"
# Upload from stdin
echo "pipeline output" | opentusk upload --stdin --name output.txt --vault "My Vault"

OpenTusk uses a presigned URL flow — file bytes never touch the API server. They go directly to S3-compatible object storage.

  1. Request upload — Your client sends file metadata (name, size, MIME type, vault) to the API. The API returns a presigned URL and a file ID.

  2. Upload bytes — Your client PUTs the raw file bytes directly to the presigned URL.

  3. Confirm — Your client tells the API the upload is complete. The API verifies the file exists in storage and sets the status to hot.

The SDK and CLI handle all three steps automatically. If you’re building a custom integration, here’s the manual flow:

Terminal window
# Step 1: Request presigned URL
curl -X POST https://api.opentusk.ai/api/files/upload \
-H "Authorization: Bearer otk_your_key" \
-H "Content-Type: application/json" \
-d '{"name": "report.pdf", "mimeType": "application/pdf", "sizeBytes": 204800, "vaultId": "vault-uuid"}'
# → {"fileId": "file-uuid", "uploadUrl": "https://...?X-Amz-Signature=..."}
# Step 2: PUT file bytes to presigned URL
curl -X PUT "<uploadUrl>" \
-H "Content-Type: application/pdf" \
--data-binary @report.pdf
# Step 3: Confirm upload
curl -X POST https://api.opentusk.ai/api/files/file-uuid/confirm \
-H "Authorization: Bearer otk_your_key"
# → {"id": "file-uuid", "status": "hot", ...}

Pay-per-upload (PPU) lets the caller pay for a single file in WAL on the Sui blockchain. It is a per-upload mode, not a plan: enabled on plans where plan.ppuEnabled === true (currently just Free) and opted into per-upload via paymentType: 'ppu' / CLI --ppu. Paid plans don’t expose PPU — storage is included. When the API receives an upload request with PPU semantics on a plan that allows it, it returns a 402 Payment Required response with a cost quote instead of a presigned URL. If the user’s plan doesn’t allow PPU, the API returns 403 Forbidden instead.

Client API Sui Blockchain
│ │ │
├─ POST /files/upload ────────►│ │
│ ├─ Calculate cost │
│◄─ 402 + cost quote ─────────┤ │
│ │ │
├─ Display cost to user │ │
├─ Sign WAL transfer ─────────────────────────────────────────►│
│◄─ Transaction digest ───────────────────────────────────────┤│
│ │ │
├─ POST /files/upload ────────►│ │
│ + quoteId + txDigest ├─ Verify payment on-chain ─────►│
│ │◄─ Confirmed ──────────────────┤│
│◄─ 200 + presigned URL ──────┤ │
│ │ │
├─ PUT bytes to URL │ │
├─ POST /confirm │ │

When the API returns 402, it includes a full cost breakdown:

{
"status": 402,
"quoteId": "quote-uuid",
"walAmountFrost": "6700000",
"walAmountFormatted": "0.0067 WAL",
"walPriceUsd": 0.10,
"usdEquivalent": "$0.00067",
"recipient": "0x...",
"expiresAt": "2026-04-05T12:00:00Z",
"breakdown": {
"walrusStorage": "0.004 WAL",
"walrusUpload": "0.001 WAL",
"doSpaces": "0.0005 WAL",
"egress": "0.0002 WAL",
"suiGas": "0.005 SUI",
"platformFee": "0.001 WAL"
}
}

Quotes expire 5 minutes after issuance. If the client doesn’t sign and retry within that window, the next upload attempt receives a new 402 with a fresh quote — there’s no penalty, but quoteId is single-use against a single txDigest.

If the API receives quoteId + txDigest but on-chain verification fails (wrong recipient, wrong amount, transaction not found yet, etc.), it returns another 402 with a fresh quote rather than a 4xx — the client should display the new cost and retry, not surface this as a hard error.

Pass --ppu to opt a single file into PPU. Without --ppu, the upload is billed against the plan’s storage quota (and on Free, that’s a 10 MB cap and hot-only storage). With --ppu, the server returns 402 with a quote; the CLI displays the cost and prompts to sign.

Terminal window
# Free plan — small file, billed against plan quota
opentusk upload small.txt --vault "My Vault"
# Any plan with ppuEnabled (today: Free) — opt this upload into PPU
opentusk upload large-file.zip --vault "My Vault" --ppu --epochs 30
# → Cost: 0.0067 WAL (~$0.00067)
# → Approve payment? (y/n)
# Auto-approve for scripts and automation
opentusk upload large-file.zip --vault "My Vault" --ppu --yes

The CLI signs a Sui transaction to transfer WAL tokens to OpenTusk’s recipient address, then retries the upload request with the quoteId and txDigest as proof of payment.

The SDK returns the 402 as a PaymentRequired object instead of throwing. Pass paymentType: 'ppu' to opt a single upload into PPU regardless of plan:

const result = await opentusk.files.requestUpload({
name: 'report.pdf',
mimeType: 'application/pdf',
sizeBytes: 204800,
vaultId: '<vault-id>',
paymentType: 'ppu', // required to opt this upload into PPU; omit for plan-quota billing
epochs: 30,
});
if ('status' in result && result.status === 402) {
// PaymentRequired — sign and pay, then retry
console.log(result.walAmountFormatted); // "0.0067 WAL"
console.log(result.recipient); // Sui address
console.log(result.breakdown); // cost breakdown
// After signing the Sui transaction:
const retry = await opentusk.files.requestUpload({
name: 'report.pdf',
mimeType: 'application/pdf',
sizeBytes: 204800,
vaultId: '<vault-id>',
paymentType: 'ppu',
epochs: 30,
quoteId: result.quoteId,
txDigest: '<sui-tx-digest>',
});
// retry now has fileId + uploadUrl
}

Without paymentType: 'ppu', subscription accounts (Developer, Scale, Enterprise) get a presigned URL directly — no 402 flow, no on-chain payment. Storage counts against the plan’s quota and hot-retention is governed by hotRetentionDays. Subscription accounts can still mix PPU files in alongside subscription files by passing paymentType: 'ppu' on the uploads they want billed on-chain.

Small files can be packed into a single Walrus blob — a bundle, backed by a Walrus Quilt — which reduces Sui gas and storage cost substantially.

  • PPU uploads (any plan) can opt in per request via the “Bundle files” toggle in the web uploader (shown when 2+ files are selected) or via POST /api/files/batch-upload. One aggregated 402 quote, one Sui payment, one Walrus write.
  • Subscription uploads get automatic async batching: files under 10 MB accumulate in a pending pool and flush when total ≥ 100 MB or oldest file is > 1 hour old.

Quilted files have a quiltPatchId instead of a walrusBlobId and cannot be individually renewed or deleted. Downloads work identically — opentusk download <file-id> handles the patch lookup automatically. See the Bundles guide for constraints, SEAL behavior, and the POST /api/files/batch-upload / batch-confirm endpoints.

OpenTusk handles encryption differently depending on vault visibility.

No encryption. Files are stored and served as plaintext.

Files uploaded to shared vaults are encrypted client-side before upload using the SEAL protocol. The server never sees plaintext.

The CLI, SDK (via MCP server), and web app handle this automatically:

  1. The client fetches the vault’s SEAL identity and key server configuration
  2. The file is encrypted with AES-256-GCM using a random data key
  3. The data key is encrypted with SEAL IBE (identity-based encryption) tied to the vault’s on-chain Whitelist
  4. The encrypted file bytes and SEAL metadata are uploaded together
  5. On download, the client’s Sui key proves membership on-chain, SEAL key servers release the decryption shares, and the file is decrypted locally
Terminal window
# Encryption is automatic — just upload to a shared vault
opentusk upload secrets.pdf --vault "Team Vault"
# → SEAL-encrypted before upload
# Download decrypts automatically (requires Sui key)
opentusk download <file-id> --output secrets.pdf

Every file has a status that reflects where it is in the storage pipeline.

StatusHot cacheWalrusDownloadableDescription
uploadingPendingNoNoUpload initiated, not yet confirmed
hotYesNoYes (fast)Confirmed, available in hot cache
syncedYesYesYes (fast)Synced to Walrus, still in hot cache
coldNoYesAfter rehydrationEvicted from hot cache, Walrus only
errorYesFailedYes (fast)Walrus sync failed, file safe in hot cache
Terminal window
# Check status once
opentusk file status <file-id>
# Poll until synced or error (max 5 minutes)
opentusk file status <file-id> --wait
# Watch all files in a vault (refreshes every 3 seconds)
opentusk ls --follow

For PPU files, the status output also includes payment type, total epochs paid (initial + extensions), the absolute expiry date, epochs remaining, and auto-renew state:

File: report.pdf
Status: synced
Walrus Blob: AoHT9eo4JpOvA6xW-ndMcpCg8GNhMtb0xKuJw0kv2hI
Payment: PPU
Epochs paid: 30
Expires: 6/18/2026, 3:02:29 PM (29 epochs remaining)
Auto-renew: On
uploading ──► hot ──► synced ──► cold
│ │
│ rehydrate
│ │
│ ◄────┘
error ◄── (any state on failure)

When you upload a file, it goes to the hot cache (S3-compatible object storage on DigitalOcean Spaces). It’s immediately available for download at full speed.

A background worker picks up the file and publishes it to the Walrus decentralized storage network. This typically takes seconds to a few minutes depending on file size. Once confirmed, the status changes to synced.

The file now exists in two places — hot cache (fast) and Walrus (durable). Two identifiers are stored:

  • walrusBlobId — content-addressed hash, used to retrieve the file from any Walrus aggregator
  • walrusBlobObjectId — Sui object ID, used for epoch renewals
Terminal window
# View Walrus metadata
opentusk file info <file-id>
# → Walrus Blob ID: Bx7K9...abc
# → Walrus Object ID: 0x1234...5678
# → Status: synced

After the hot retention period expires, the hot eviction worker removes the file from the hot cache. The file is now cold — stored only on Walrus.

  • Subscription plans have a fixed retention period (e.g., 7 days, 30 days)
  • PPU files (on any plan) keep hotUntil = ppuEpochEnd; extending epochs moves both forward

When you download a cold file, OpenTusk automatically fetches it from Walrus back to the hot cache. There’s no separate rehydration step — requesting a download triggers it:

Terminal window
# Downloading a cold file triggers rehydration automatically
# The CLI polls until the file is ready (up to 60 seconds)
opentusk download <file-id> --output report.pdf
# Direct rehydration from Walrus by blob ID (no API key needed)
opentusk file rehydrate Bx7K9...abc --output myfile.enc

If Walrus sync fails after retries, the file status is set to error. The file remains safe in the hot cache — it just hasn’t been durably stored yet.

Terminal window
# Retry a specific file
opentusk file retry <file-id>
# Retry all files in error status
opentusk file retry

Walrus stores data for a set number of epochs (each ~24 hours). When epochs expire, the data is garbage collected permanently by the network.

OpenTusk automatically renews epoch leases before they expire via a background worker (walrusRenew). Renewals use the walrusBlobObjectId to call extendBlob() on the Sui blockchain.

PPU files can be extended at any time before they expire via an on-chain 402 flow that mirrors the upload flow. Auto-renewal can be toggled separately:

Terminal window
# Purchase additional epochs (signs and pays on-chain, then enqueues the
# extension job). Acknowledges 202 once the worker is queued.
opentusk file extend <file-id> --epochs 30
# Enable auto-renewal
opentusk file auto-renew <file-id> --on
# Disable auto-renewal
opentusk file auto-renew <file-id> --off
Terminal window
# Download to current directory (original filename)
opentusk download <file-id>
# Download to specific path
opentusk download <file-id> --output ./downloads/report.pdf
# Stream to stdout (for pipes and sandboxed environments)
opentusk download <file-id> --stdout
opentusk download <file-id> --stdout > report.pdf

Shared vault files are decrypted automatically by the CLI and MCP server (requires Sui key).

When @opentusk/indexer is installed, the CLI and MCP tools notify it with plaintext bytes during upload and download — files get semantically indexed for local search as a side effect of normal workflows. No extra step is required. See the Semantic Search guide for details.

Every file record carries an uploaderSuiAddress field — the Sui address of the identity that uploaded the file. In shared vaults, this lets members see who contributed each file. For personal vaults it usually matches the account owner’s Sui address; for files uploaded by an agent, it’s the agent’s address.

Terminal window
# List all files
opentusk ls
# List files in a specific vault
opentusk ls "My Vault"
# Sort by name, size, or date
opentusk ls --sort name --limit 100
# Filter by folder
opentusk ls --folder <folder-id>

Files are soft-deleted (moved to trash) with a 7-day grace period before permanent deletion.

Terminal window
# Soft-delete (moves to trash)
opentusk file delete <file-id>
opentusk file delete <file-id> --yes # skip confirmation
# Delete multiple files at once
opentusk file delete <id1> <id2> <id3> --yes
Terminal window
# List trashed items
opentusk trash list
# Restore from trash
opentusk trash restore <id>
# Permanently delete one item
opentusk trash delete <id> --yes
# Empty all trash
opentusk trash empty --yes
Terminal window
# Move to a folder
opentusk file move <file-id> --folder <folder-id>
# Move back to vault root
opentusk file move <file-id> --root

Webhooks notify your systems when files change status. Subscribe to the events you care about:

EventTriggered when
file.hotFile confirmed and available in hot cache
file.syncedFile synced to Walrus
file.coldFile evicted from hot cache
file.errorWalrus sync or processing failed
file.deletedFile soft-deleted (moved to trash)
file.rehydratedCold file rehydrated back to hot cache
Terminal window
# Create a webhook
opentusk webhook create https://example.com/hook --events file.synced,file.error
# Test it
opentusk webhook test <webhook-id>
# View deliveries
opentusk webhook deliveries <webhook-id>

Webhook payloads include the file ID, new status, vault ID, and timestamp. Verify signatures using the webhook secret with HMAC-SHA256. See the Webhooks guide for payload format and verification.

All uploads are checked against your plan’s quotas:

CheckWhat happens if exceeded
Storage quotaUpload rejected with 409 Storage quota exceeded
Max file sizeUpload rejected with 413 File size limit exceeded
Max vaultsVault creation rejected

Quota is calculated on plaintext size — you’re not penalized for encryption overhead on shared vault files.

Terminal window
# Check your current usage
opentusk account usage
# See plan limits
opentusk account plan