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/api
All endpoints below are relative to this base. Example: POST /upload/init means POST https://storage.to/api/upload/init.
Authentication
Most endpoints don't require authentication. Anonymous uploads are a core feature.
Authentication is optional and unlocks:
- 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
We use Laravel Sanctum bearer tokens. Issue a token via the desktop OAuth handoff or the web login, then send it as:
Authorization: Bearer <token>
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.
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/init
60/min
Initiate an upload. For files >50 MB the response includes part_urls for a multipart upload; otherwise a single url.
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/parts
120/min
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. |
/upload/complete-multipart
500/min
Finalize 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/abort
60/min
Cancel 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/confirm
60/min
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. |
/file/reserve
60/min
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. |
/upload/init-batch
500/min
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.
/upload/confirm-batch
500/min
Batch 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.
/collection
30/min
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. |
/collection/{id}/status
120/min
Poll the state of a collection. Also auto-marks the collection ready if all expected files have confirmed.
/collection/{id}/ready
Owner only
60/min
Mark the collection as ready for download. Not usually needed — collections auto-ready once expected_file_count is reached.
/collection/{id}
Owner only
60/min
Delete a collection and all its files.
/collection/{id}/password
Owner only
30/min
Set a password on the collection. Requires 4–100 chars.
Request body
| Field | Type | Description |
|---|---|---|
password | string · required | 4–100 chars. |
/collection/{id}/password
Owner only
30/min
Remove the password from a collection.
/collection/{id}/verify-password
10/min
Check a password. Returns 200 on success, 401 on incorrect password.
Request body
| Field | Type | Description |
|---|---|---|
password | string · required |
/collection/{id}/expiry
Owner only
30/min
Change 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-downloads
Owner only
30/min
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
All file-level settings (password, expiry, max-downloads) mirror the collection endpoints. Owner-only.
/file/{id}/status
120/min
Check whether a file is still pending its upload.
/file/{id}
Owner only
60/min
Delete a file immediately.
/file/{id}/thumbnail
Owner only
120/min
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. |
/file/{id}/password
Owner only
30/min
Set a password on a file. Requires 4–100 chars.
/file/{id}/password
Owner only
30/min
Remove a file's password.
/file/{id}/verify-password
10/min
Verify a file's password.
/file/{id}/expiry
Owner only
30/min
Change 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-downloads
Owner only
30/min
Cap 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.
/user
Bearer token
Return the authenticated user.
/auth/logout
Bearer token
Revoke the current access token.
Misc
/health
Health check. Pings the database, R2 storage, and Redis cache. Returns 200 if all green, 503 otherwise.
/activity
Live activity stream for the homepage globe. Cached at Cloudflare's edge.
/bandwidth/status
60/min
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.
/app-analytics
120/min
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. |
/app-errors
60/min
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. |