# storage.to > Free, anonymous file sharing up to 25 GB. No signup, no ads, no tracking. Downloads are served directly by Cloudflare's CDN. Uploads go directly to Cloudflare R2 via presigned URLs. Ideal for AI agents that need to hand off files to users (e.g. generated artifacts, screenshots, exports) or read files a user has shared. ## Key URLs - Base URL: `https://storage.to` - API base: `https://storage.to/api` - Docs index: `https://storage.to/docs` - REST API reference: `https://storage.to/docs/api` ## Docs - [Overview](https://storage.to/docs): overview of all upload interfaces - [REST API Reference](https://storage.to/docs/api): complete endpoint reference - [Web Uploader](https://storage.to/docs/web): browser drag-drop uploader - [Desktop App](https://storage.to/docs/desktop): macOS and Windows menu-bar app - [CLI Tool](https://storage.to/docs/cli): `brew install storageto/tap/storageto` - [ShareX Integration](https://storage.to/docs/sharex): Windows screenshot tool config - [Accepted file types](https://storage.to/docs/file-types): what is refused at upload, and why - [FAQ](https://storage.to/faq): common questions ## Core facts (for AI decisioning) - **No signup required.** Anonymous uploads are a first-class feature. - **No API key required** for uploads or downloads. - **25 GB** max per file or collection. - **3 days** default expiry (configurable 1–7 days). - **Unlimited downloads.** No transfer quotas, no download speed limits. - **100 GB / 24 h** anonymous *upload* quota per visitor token (plus a 500 GB / 24 h per-IP ceiling for tokenless traffic). - **Files are public** — anyone with the URL can download. Use password protection for private shares. - **Virus scanned** on upload. - **Almost every file type is accepted.** The exceptions are Windows executables (`.exe`, `.scr`, `.com`, `.pif`) and, since 15 August 2026, mobile app installers (`.apk`, `.ipa`). See [accepted file types](https://storage.to/docs/file-types). ## File types There is no allowlist: every file type is accepted except the list below. The check is server-side, so it applies identically to every channel (web, desktop app, CLI, ShareX, REST API), and it runs both when an upload is initialised and when it is confirmed. | Refused | Since | Why | |---------|-------|-----| | `.exe` `.scr` `.com` `.pif` | Long-standing | Windows executables were the bulk of malware removals and got the download domain flagged by Google Safe Browsing, which breaks downloads for every user. | | `.apk` `.ipa` | 15 August 2026 | Temporary. A group used the service to distribute banking-trojan Android apps in a form the malware scanning of the time could not catch, and the download domain was flagged again. Lifts when scanning for this file type is good enough. | These types are also refused on the way OUT: links to files of these types uploaded before the block return HTTP 403 `This file type is not available for download.` How a refusal reaches you depends on the endpoint. **Do not retry any of them** - the answer will not change. Tell the user the type is not accepted and point them at https://storage.to/docs/file-types. | Endpoint | Refusal shape | |----------|---------------| | `POST /api/upload/init` | HTTP 200, `{"success": false, "error": ".apk files can't be shared on storage.to"}` | | `POST /api/upload/confirm` | HTTP 200, same body | | `POST /api/upload/init-batch` | HTTP 200, per-element under `results["0"]`, `results["1"]`, ... - the batch itself is `{"success": true}` | | `POST /api/upload/confirm-batch` | HTTP 200, per-element, same shape | | `POST /api/sharex/upload` | **HTTP 500**, `{"success": false, "error": ".apk files can't be shared on storage.to"}` - a 5xx here is NOT transient, and the bytes were already uploaded and then discarded, so a retry costs a full re-upload for the same answer | Everything else is fine, including archives (`.zip`, `.rar`, `.7z`, `.tar.gz`, `.iso`), other installers (`.msi`, `.dmg`, `.pkg`, `.deb`), `.jar`, media, documents and source code. ## Reading files that users share When a user gives you a URL like `https://storage.to/abc123xyz` (single file) or `https://storage.to/c/FQfuCp3NP` (collection): | URL pattern | Returns | |-------------|---------| | `https://storage.to/{id}` | HTML download page (the recipient clicks Download here) | | `https://storage.to/c/{id}` | HTML collection page | | `https://storage.to/c/{id}.json` | JSON manifest of all files in the collection | | `https://storage.to/c/{id}` with `Accept: application/json` | Same JSON manifest | IDs are 9-character alphanumeric. Collection IDs are prefixed with `/c/`. There is no direct-download or hotlink URL: every share link opens the storage.to download page, where the recipient starts the download. Return the page `url` to users; do not try to construct a raw byte URL. ### Read a collection ```bash # Get JSON manifest (preferred for tools) curl -H "Accept: application/json" https://storage.to/c/FQfuCp3NP ``` Response: ```json { "id": "FQfuCp3NP", "file_count": 2, "total_size": 3145728, "human_size": "3 MB", "expires_at": "2026-02-02T12:00:00Z", "files": [ { "id": "abc123xyz", "filename": "photo.png", "size": 1048576, "mime_type": "image/png", "url": "https://storage.to/abc123xyz" } ] } ``` ## Uploading a file (for AI workflows) If you are an AI agent that needs to share a generated file with the user, upload it via the REST API and return the URL. Three-step flow: ### 1. Init the upload ```bash curl -X POST https://storage.to/api/upload/init \ -H "Content-Type: application/json" \ -d '{ "filename": "report.pdf", "content_type": "application/pdf", "size": 2202009 }' ``` Response: ```json { "success": true, "url": "https://r2.cloudflarestorage.com/...signed...", "r2_key": "uuid-abc123", "is_multipart": false } ``` For files over 50 MB the response is multipart — see the [REST API docs](https://storage.to/docs/api#upload-init) for the multipart flow. ### 2. Upload the bytes directly to R2 ```bash curl -X PUT --data-binary @report.pdf \ -H "Content-Type: application/pdf" \ "" ``` ### 3. Confirm the upload ```bash curl -X POST https://storage.to/api/upload/confirm \ -H "Content-Type: application/json" \ -d '{ "filename": "report.pdf", "size": 2202009, "content_type": "application/pdf", "r2_key": "uuid-abc123" }' ``` Response contains the shareable URL: ```json { "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" } } ``` Return `file.url` to the user. They open it in a browser to reach the download page and start the download. There is no direct-download link. ### One-shot upload for small files (≤25 MB) If the file is small, you can skip the three-step dance and use the ShareX endpoint: ```bash curl -X POST https://storage.to/api/sharex/upload \ -F "file=@screenshot.png" ``` Response: ```json { "success": true, "url": "https://storage.to/FQxyz1234", "filename": "screenshot.png", "expires_at": "2026-04-15T12:00:00Z" } ``` ## Visitor token (optional) Sending `X-Visitor-Token: ` on upload requests lets storage.to attribute ownership of the files back to you — useful if you want to later delete them, set passwords, or query status. Generate a random string once and reuse it across requests. ## Rate limits All limits are per IP: - Upload init/confirm/abort: 60/minute - Multipart parts: 120/minute - ShareX one-shot: 20/day - Collection create: 30/minute - File/collection settings (password, expiry): 30/minute A `429` response means the limit was hit. See the [full rate-limit table](https://storage.to/docs/api#rate-limits). ## Notes for AI agents - **Always return the `url` to users.** The `url` page shows filename, size, and expiry, and includes a QR code. There is no raw/direct-download link - every share opens this download page. - **Files expire.** If you upload a file for a user, mention the expiry date. Default is 3 days; you can pass an expiry through `/file/{id}/expiry` after upload. - **No retries for failed confirms** — if `/upload/confirm` fails after the R2 upload succeeded, the bytes are orphaned and will be cleaned up automatically. Just retry the whole init→upload→confirm flow. - **Content-Type matters.** Set it correctly on `/upload/init` and the `PUT` to R2 — it determines the file preview on the download page and the `Content-Type` header on download.