REST API
Upload files, create collections, and manage shares over HTTP. All responses are JSON; no API key is required for anonymous uploads.
How uploads work
The storage.to API powers our CLI, desktop app, web uploader, and any third-party client you want to build. The upload flow is three steps:
PUT your bytes directly to the presigned URL(s). Bytes don't pass through our servers.File record and hand you a shareable URL.All endpoints below are relative to this base. Example: POST /upload/init means POST https://storage.to/api/upload/init.
Authentication
Anonymous uploads work without authentication, but with tight limits. Keyless callers are capped at 50 files per rolling 24 hours per device or IP, plus the bandwidth quotas below, and files expire after 3 days.
Authenticating with a free account unlocks:
- No daily file cap
- No upload bandwidth quota
- Uploads attached to your account (visible at /dashboard)
- Premium features (permanent files, larger storage)
- Ownership-based mutations (delete, set password, change expiry) without needing the visitor-token match
Limits at a glance
expiry_daysexpiry_daysThere are no monthly quotas - all limits are rolling 24-hour windows or per-minute rate limits. The free plan has no fixed storage pool: every file expires on its own, so nothing accumulates against a quota.
Proving ownership
/upload/init multipart, /upload/confirm, /file/reserve, /collection) returns an owner_token in its response. The token is a signed proof of ownership tied to that specific resource, independent of your IP or visitor token. Tokens live as long as the resource does, are safe to persist, and do not expire independently. A lost token means losing control of that resource (file/collection/upload) - treat them like local passwords.# or, alongside a Bearer session
X-Owner-Token: <token>
visitor_token cookie automatically. The CLI stores it at ~/.config/storageto/token (see CLI docs).For mutation endpoints (delete, set password, change expiry), ownership is confirmed if either the visitor token matches or the request comes from the same IP that created the file. Both can be lost (cleared cookies, network changes). The owner token is the preferred proof going forward.
Errors
Errors follow a consistent shape: { "success": false, "error": "โฆ" }
Rate limits
All rate limits are per-IP. A 429 response includes standard Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers.
Upload quota: anonymous clients have two ceilings running in parallel - 100 GB / 24 h per visitor token and 500 GB / 24 h per IP (the IP ceiling catches tokenless traffic and shared networks). When either is exceeded you'll get a 429 with details. This is an upload quota only - downloads are unlimited and unthrottled.
Upload
8 endpointsThe three-step upload flow for any file, including files over 5 GB (automatically multipart). If you only need a quick screenshot-style upload, see ShareX instead.
Initiate an upload. For files >50 MB the response is a multipart upload (type: "multipart" field); otherwise a single presigned PUT.
Request body
| Field | Type | Description |
|---|---|---|
filename | string ยท required | Original filename. Max 255 chars. |
content_type | string ยท required | MIME type. |
size | integer ยท required | File size in bytes. Min 1. |
curl -X POST https://storage.to/api/upload/init \ -H "Content-Type: application/json" \ -H "X-Visitor-Token: abc123" \ -d '{ "filename": "report.pdf", "content_type": "application/pdf", "size": 2202009 }'
{ "success": true, "type": "single", "upload_url": "https://r2.cloudflarestorage.com/...signed...", "headers": { "Host": ["..."] }, "r2_key": "uuid-abc123" }
{ "success": true, "type": "multipart", "upload_id": "01HXYZ...", "r2_key": "uuid-abc123", "part_size": 33554432, "total_parts": 4, "initial_urls": { "1": "https://...", "2": "https://..." }, "owner_token": "owner_v1_..." }
Request additional part URLs for an in-progress multipart upload. Used when /init returned fewer URLs than you have parts (or they expired).
Request body
| Field | Type | Description |
|---|---|---|
upload_id | string ยท required | The upload_id from /init. |
part_numbers | array<int> ยท required | Part numbers to get URLs for. |
curl -X POST https://storage.to/api/upload/parts \ -H "Content-Type: application/json" \ -d '{ "upload_id": "01HXYZ...", "part_numbers": [3, 4] }'
{ "success": true, "part_urls": [ { "partNumber": 3, "url": "https://..." }, { "partNumber": 4, "url": "https://..." } ] }
Finalize a multipart upload once all parts are uploaded.
Request body
| Field | Type | Description |
|---|---|---|
upload_id | string ยท required | The upload_id from /init. |
parts | array ยท required | Each entry: { partNumber, etag } from the part upload response. |
curl -X POST https://storage.to/api/upload/complete-multipart \ -H "Content-Type: application/json" \ -d '{ "upload_id": "01HXYZ...", "parts": [ { "partNumber": 1, "etag": "\"abc...\"" }, { "partNumber": 2, "etag": "\"def...\"" } ] }'
{ "success": true }
Cancel a multipart upload and clean up any partial data.
Request body
| Field | Type | Description |
|---|---|---|
upload_id | string ยท required | The upload to abort. |
curl -X POST https://storage.to/api/upload/abort \ -H "Content-Type: application/json" \ -d '{ "upload_id": "01HXYZ..." }'
Confirm the upload is complete. This is when we create the File record and return the shareable URL.
Request body
| Field | Type | Description |
|---|---|---|
filename | string ยท required | Original filename. |
size | integer ยท required | File size in bytes. |
content_type | string ยท required | MIME type. |
r2_key | string ยท required | The r2_key from /init. |
collection_id | string ยท optional | Attach to a collection. |
crc32 | integer ยท optional | CRC32 checksum for integrity verification. |
file_id | string(9) ยท optional | Fulfil a previously reserved file ID. |
curl -X POST https://storage.to/api/upload/confirm \ -H "Content-Type: application/json" \ -H "X-Visitor-Token: abc123" \ -d '{ "filename": "report.pdf", "size": 2202009, "content_type": "application/pdf", "r2_key": "uuid-abc123" }'
{ "success": true, "file": { "id": "FQxyz1234", "url": "https://storage.to/FQxyz1234", "filename": "report.pdf", "size": 2202009, "human_size": "2.1 MB", "expires_at": "2026-04-15T12:00:00Z" }, "owner_token": "owner_v1_..." }
Reserve a file ID and shareable URL before the bytes are ready. Useful when you need to hand out a link first and fulfil the upload afterwards. Ownership is bound to your visitor token + IP. Finish the upload later with /upload/init + /upload/confirm, passing file_id to confirm.
Request body
| Field | Type | Description |
|---|---|---|
filename | string ยท optional | Placeholder filename. Defaults to "Pending". |
content_type | string ยท optional | Placeholder MIME type. |
curl -X POST https://storage.to/api/file/reserve \ -H "X-Visitor-Token: abc123"
{ "success": true, "file": { "id": "FQxyz1234", "url": "https://storage.to/FQxyz1234", "expires_at": "2026-04-12T18:00:00Z" }, "owner_token": "owner_v1_..." }
Batch equivalent of /upload/init, optimised for the web uploader. Initiates up to 250 files in one round-trip.
Used internally by the web uploader. Most clients should prefer single-file /upload/init.
Batch equivalent of /upload/confirm. Confirms many files in one round-trip.
Collections
9 endpointsA collection groups multiple files under a single share URL (/c/{id}). Up to 10,000 files and 25 GB total.
Create a new collection. Attach files afterwards by passing collection_id on /upload/confirm.
Request body
| Field | Type | Description |
|---|---|---|
expected_file_count | integer ยท optional | Hint for auto-marking the collection ready once all expected files have confirmed. |
curl -X POST https://storage.to/api/collection \ -H "Content-Type: application/json" \ -H "X-Visitor-Token: abc123" \ -d '{ "expected_file_count": 3 }'
{ "success": true, "collection": { "id": "ABC123xyz", "url": "https://storage.to/c/ABC123xyz", "expires_at": "2026-04-15T12:00:00Z" }, "owner_token": "owner_v1_..." }
Poll the state of a collection. Also auto-marks the collection ready if all expected files have confirmed.
curl https://storage.to/api/collection/ABC123xyz/status
{ "success": true, "files": [ /* file objects: id, url, filename, size, ... */ ], "is_uploading": false, "file_count": 3, "expected_file_count": 3, "total_size": 6291456, "human_total_size": "6 MB" }
Mark the collection as ready for download. Not usually needed - collections auto-ready once expected_file_count is reached.
Delete a collection and all its files.
Set a password on the collection. Requires 4โ100 chars.
Request body
| Field | Type | Description |
|---|---|---|
password | string ยท required | 4โ100 chars. |
curl -X POST https://storage.to/api/collection/ABC123xyz/password \ -H "X-Visitor-Token: abc123" \ -d '{ "password": "hunter22" }'
Remove the password from a collection.
Check a password. Returns 200 on success, 401 on incorrect password.
Request body
| Field | Type | Description |
|---|---|---|
password | string ยท required |
Change a collection's expiration.
Request body
| Field | Type | Description |
|---|---|---|
days | integer ยท optional | 1โ7 days from now, but never later than 7 days after upload (or the current expiry, if later). Omit or null for permanent (premium only). |
Set a download cap (burn-after-N-downloads). Collection auto-deletes when reached.
Request body
| Field | Type | Description |
|---|---|---|
max_downloads | integer ยท optional | 1โ1000. Must exceed current download count. null to remove the cap. |
Files
8 endpointsAll file-level settings (password, expiry, max-downloads) mirror the collection endpoints. Owner-only.
Check whether a file is still pending its upload.
{ "pending": false }
Delete a file immediately.
Upload a thumbnail image for a video or image file (used on the download page). Max 2 MB.
Request body
| Field | Type | Description |
|---|---|---|
thumbnail | image ยท required | Multipart upload. Max 2 MB. |
{ "success": true, "thumbnail_url": "https://..." }
Set a password on a file. Requires 4โ100 chars.
Remove a file's password.
Verify a file's password.
Change a file's expiration.
Request body
| Field | Type | Description |
|---|---|---|
days | integer ยท optional | 1โ7 days from now, but never later than 7 days after upload (or the current expiry, if later). Omit or null for permanent (premium only). |
Cap a file's total downloads. Auto-deletes when reached.
Desktop auth
2 endpointsFor signed-in clients, such as the desktop app, that hold a bearer token.
Return the authenticated user.
curl https://storage.to/api/user \ -H "Authorization: Bearer <token>"
{ "id": 42, "name": "Ada", "email": "ada@example.com", "is_premium": true }
Revoke the current access token.
Misc
5 endpointsHealth, quotas and client telemetry.
Liveness check. Returns 200 with { "status": "ok" } when the API worker is serving.
{ "status": "ok" }
Live activity stream for the homepage globe. Cached at the edge.
Current upload quota usage for the caller - used by the CLI and desktop app to show remaining capacity. Response shape differs for authenticated users. Despite the URL name, this tracks upload bytes only; downloads aren't counted.
{ "success": true, "authenticated": false, "has_token": true, "limit_bytes": 107374182400, "limit_gb": 100, "used_bytes": 12345678, "used_gb": 0.01, "remaining_bytes": 107361836722, "remaining_gb": 99.99, "window_hours": 24 }
{ "success": true, "authenticated": true, "plan": "premium" }
Submit a usage event from the CLI or desktop app.
Request body
| Field | Type | Description |
|---|---|---|
app | string ยท required | desktop, cli, or web. |
version | string ยท optional | Client version. |
event | string ยท required | Event name, e.g. upload_complete. |
context | object ยท optional | Extra metadata. |
Submit an error report from the CLI or desktop app. Deduplicated server-side - max 10 of the same error per hour.
Request body
| Field | Type | Description |
|---|---|---|
app | string ยท required | desktop, cli, or web. |
type | string ยท required | Error class/type. |
message | string ยท required | Error message. |
stack | string ยท optional | Stack trace. |
version, os, os_version, arch, context | various ยท optional | Diagnostic metadata. |