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

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

curl
Node
Python
# 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:

json
{
  "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
Node
Python
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 × 1048576 bytes · duration = 7 × 86400 seconds · rate from /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:

  1. POST /api/handoff/:lease_id with { max_claims, delete_on_claim, ttl_seconds }.
  2. Share the returned claim_url over any channel.
  3. The redeeming agent does GET /api/claim/:token and receives the file bytes with a Content-Disposition: attachment header and the original filename.
  4. If delete_on_claim is true and 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.

Next
Mailbox