Storage.
A Storage lease is the configurable primitive. You specify size, duration, and access rules. The file is there for exactly the lease you paid for — no more, no less. Call this when you want fine-grained control.
The other variants (Mailbox, Checkpoint) are opinionated shortcuts that wrap a Storage-flavored lease in a specific workflow. When you don't want the shortcut, you want Storage.
When to use it
- You have a file you want to park somewhere for a known duration and hand off to another agent (or to yourself, later).
- You need the underlying object to be read more than once before expiry — by the owner, by handoff-token holders, or both.
- You want to bring your own access policy (multi-claim handoffs, long TTLs, custom delete-on-access rules) instead of accepting Mailbox/Checkpoint defaults.
Create a lease
The request body is the raw file bytes. Size, duration, and filename ride in headers so the server can price and write in one pass. Authenticate with either Authorization: Bearer rs_live_… (account) or an x402 X-Payment header (wallet).
# 1 MB file, 1 hour, paid from your prepaid balance curl -X POST https://relaystation.ai/api/store \ -H "Authorization: Bearer rs_live_YOUR_KEY" \ -H "X-File-Name: notes.txt" \ -H "X-Size-Bytes: 1048576" \ -H "X-Duration-Seconds: 3600" \ -H "Content-Type: text/plain" \ --data-binary @notes.txt
// Node 18+ (native fetch, Blob from fs) import { readFileSync, statSync } from 'node:fs'; const file = readFileSync('notes.txt'); const res = await fetch('https://relaystation.ai/api/store', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RS_KEY}`, 'X-File-Name': 'notes.txt', 'X-Size-Bytes': String(file.length), 'X-Duration-Seconds': '3600', 'Content-Type': 'text/plain', }, body: file, }); const lease = await res.json(); console.log(lease.lease_id);
# Python 3.10+ with `requests` import os, requests, pathlib path = pathlib.Path('notes.txt') body = path.read_bytes() res = requests.post( 'https://relaystation.ai/api/store', headers={ 'Authorization': f'Bearer {os.environ["RS_KEY"]}', 'X-File-Name': path.name, 'X-Size-Bytes': str(len(body)), 'X-Duration-Seconds': '3600', 'Content-Type': 'text/plain', }, data=body, ) lease = res.json() print(lease['lease_id'])
Either path returns the same lease shape:
{
"status": "ok",
"actor_type": "account",
"lease_id": "lse_01HX…",
"expires_at": "2026-04-19T22:00:00.000Z",
"duration_seconds": 3600,
"duration_human": "1.0 hour",
"size_bytes": 1048576,
"cost_micros": "29",
"paid_usdc": 0.000029,
"ledger_id": "led_01HX…"
}
Read a lease back
The owner can download the file any number of times before expiry. Authenticate with the same credential used at creation.
curl https://relaystation.ai/api/download/lse_01HX… \
-H "Authorization: Bearer rs_live_YOUR_KEY" \
-o notes.txt
const res = await fetch( `https://relaystation.ai/api/download/${leaseId}`, { headers: { Authorization: `Bearer ${process.env.RS_KEY}` } }, ); const bytes = new Uint8Array(await res.arrayBuffer());
res = requests.get( f'https://relaystation.ai/api/download/{lease_id}', headers={'Authorization': f'Bearer {os.environ["RS_KEY"]}'}, ) pathlib.Path('notes.txt').write_bytes(res.content)
List your leases
GET /api/leases returns every lease owned by the authenticated actor — active, expired, and deleted — with per-row status and access history. Useful for dashboards, reconciliation, or audit.
Pricing
Storage uses bytes_seconds pricing — size × duration × rate. You pay once at lease creation. No recurring charges; leases do not renew.
The live rate and duration bounds are returned by GET /api/pricing. One worked example:
100 MB for 1 week.
size = 100 × 1048576bytes ·duration = 7 × 86400seconds ·ratefrom/api/pricing.
At the default seeded rate, the charge is a fraction of a cent. Minimum charge floor applies — see the live API for the current minimum.
Handing off a Storage lease
Handoff is shipped for Storage today. See Handoff — the feature for the full conceptual flow; the storage-specific surface is:
POST /api/handoff/:lease_idwith{ max_claims, delete_on_claim, ttl_seconds }.- Share the returned
claim_urlover any channel. - The redeeming agent does
GET /api/claim/:tokenand receives the file bytes with aContent-Disposition: attachmentheader and the original filename. - If
delete_on_claimistrueand this was the final claim, the underlying object is deleted after the stream completes.
GET /api/handoffs/:lease_id lists outstanding tokens for a lease — token prefix, claims remaining, expiry, delete-on-claim flag. Use it for audit and for dashboard "Active tokens" rows. To revoke, call DELETE /api/handoffs/:id — idempotent, prevents future claims (any already-completed claims stand). A second revoke on the same id returns 404.
Expiry behavior
When the lease's expires_at passes, the S3 object is deleted and the lease row is tombstoned. Subsequent reads — owner or claim-token — return 410 Gone. There is no grace period, no recovery, and no way to extend a lease in flight. If you need more time, create a new lease.