REST API
Upload files, create collections, and manage shares over HTTP. All responses are JSON; no API key is required for anonymous uploads.
Introduction
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:
- Init - tell us you want to upload a file. We return one or more presigned URLs pointing at Cloudflare R2.
- Upload to R2 -
PUTyour bytes directly to the presigned URL(s). Bytes don't pass through our servers. - Confirm - tell us the upload finished. We create a
Filerecord and hand you a shareable URL.
Base URL
https://storage.to/apiAll 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
| Limit | No token (anonymous) | Free account token | Paid account token |
|---|---|---|---|
| Files per day | 50 / 24 hours | Unlimited | Unlimited |
| Upload bandwidth | 100 GB / 24 hours (500 GB per IP) | Unlimited | Unlimited |
| Max file size | 25 GB | 25 GB | 100 GB |
| File expiry | 3 days by default, up to 7 via expiry_days | 3 days by default, up to 7 via expiry_days | Never (files are permanent) |
| Storage pool | None (files expire) | None (files expire) | 100 GB - 1 TB permanent, by plan |
There 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.
To authenticate, generate a personal API token from your account and send it as a bearer token on every request:
Authorization: Bearer <token>You need to be signed in. The full token is shown only once at creation, so copy it somewhere safe. You can revoke a token at any time from the same page.
Visitor token
Anonymous clients need a way to prove ownership of their own uploads without an account. We use a visitor token - a random string the client generates once and reuses. Send it with every request:
X-Visitor-Token: <random-string>On the web, the token is stored in the 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.
Owner token
Every resource-creating endpoint (/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.
Store the token alongside the resource ID and send it on any mutation as:
Authorization: Owner <token>Or, if you are already using Authorization: Bearer for an authenticated session, send it as:
X-Owner-Token: <token>The server accepts the owner token as a valid ownership proof alongside the legacy visitor token + IP fallback - clients that hold the token keep working after switching networks or clearing cookies, and clients that do not still work exactly as before.
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.
Errors
Errors follow a consistent shape:
{
"success": false,
"error": "Human-readable message"
}Common HTTP status codes:
| Code | Meaning |
|---|---|
200 | OK. |
201 | Created. |
400 | Bad request (e.g. collection size limit exceeded). |
401 | Password required or incorrect. |
403 | Not authorized (not the owner of the resource). |
404 | Resource not found or expired. |
422 | Validation failed, or plan/quota restriction. |
429 | Rate limit or upload quota hit. |
500 | Server error. Check status. |
Rate limits
All rate limits are per-IP. A 429 response includes standard Retry-After, X-RateLimit-Limit, and X-RateLimit-Remaining headers.
| Scope | Limit |
|---|---|
| Upload init / confirm / abort | 60 / minute |
| Multipart completion | 500 / minute |
| Multipart part URLs | 120 / minute |
| Batch init / confirm | 500 / minute |
| Status polls (file & collection) | 120 / minute |
| Settings (password, expiry, max-downloads) | 30 / minute |
| Password verification | 10 / minute |
| Collection create | 30 / minute |
| Manage (ready, delete) | 60 / minute |
| Thumbnail upload | 120 / minute |
| ShareX upload | 20 / day |
| App analytics / errors | 120 and 60 / minute |
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 (served directly from R2 signed URLs).
Upload
The 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.
/upload/init60/minInitiate 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. |
/upload/partsOwner only120/minRequest 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. |
/upload/complete-multipartOwner only500/minFinalize a multipart upload on R2 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 R2 response. |
/upload/abortOwner only60/minCancel a multipart upload and clean up any partial data on R2.
Request body
| Field | Type | Description |
|---|---|---|
upload_id | string · required | The upload to abort. |
/upload/confirm60/minConfirm 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. |
/file/reserve60/minReserve 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. |
/upload/init-batch500/minBatch 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.
/upload/confirm-batch500/minBatch equivalent of /upload/confirm. Confirms many files in one round-trip.
Collections
A collection groups multiple files under a single share URL (/c/{id}). Up to 10,000 files and 25 GB total.
/collection30/minCreate 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. |
/collection/{id}/status120/minPoll the state of a collection. Also auto-marks the collection ready if all expected files have confirmed.
/collection/{id}/readyOwner only60/minMark the collection as ready for download. Not usually needed - collections auto-ready once expected_file_count is reached.
/collection/{id}Owner only60/minDelete a collection and all its files.
/collection/{id}/passwordOwner only30/minSet a password on the collection. Requires 4–100 chars.
Request body
| Field | Type | Description |
|---|---|---|
password | string · required | 4–100 chars. |
/collection/{id}/passwordOwner only30/minRemove the password from a collection.
/collection/{id}/verify-password10/minCheck a password. Returns 200 on success, 401 on incorrect password.
Request body
| Field | Type | Description |
|---|---|---|
password | string · required |
/collection/{id}/expiryOwner only30/minChange a collection's expiration.
Request body
| Field | Type | Description |
|---|---|---|
days | integer · optional | 1–7 days from now. Omit or null for permanent (premium only). |
/collection/{id}/max-downloadsOwner only30/minSet 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
All file-level settings (password, expiry, max-downloads) mirror the collection endpoints. Owner-only.
/file/{id}/status120/minCheck whether a file is still pending its upload.
/file/{id}Owner only60/minDelete a file immediately.
/file/{id}/thumbnailOwner only120/minUpload 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. |
/file/{id}/passwordOwner only30/minSet a password on a file. Requires 4–100 chars.
/file/{id}/passwordOwner only30/minRemove a file's password.
/file/{id}/verify-password10/minVerify a file's password.
/file/{id}/expiryOwner only30/minChange a file's expiration.
Request body
| Field | Type | Description |
|---|---|---|
days | integer · optional | 1–7 days from now. Omit or null for permanent (premium only). |
/file/{id}/max-downloadsOwner only30/minCap a file's total downloads. Auto-deletes when reached.
ShareX upload
One-shot upload endpoint - send a multipart file, get a shareable URL back. No init/confirm dance. Ideal for screenshot tools. Full setup guide at /docs/sharex.
Desktop auth
For authenticated clients (e.g. the desktop app) holding a Sanctum token.
/userBearer tokenReturn the authenticated user.
/auth/logoutBearer tokenRevoke the current access token.
Misc
/healthLiveness check. Returns 200 with { "status": "ok" } when the API worker is serving.
/activityLive activity stream for the homepage globe. Cached at Cloudflare's edge.
/bandwidth/status60/minCurrent 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.
/app-analytics120/minSubmit 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. |
/app-errors60/minSubmit 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. |