Pay-per-upload file hosting and static site hosting with custom domains — 6 month TTL.
Low accuracy (28%). Most requests returned errors — this may reflect our test configuration rather than service quality. We're working to improve coverage. Very reliable — 10/10 requests got a response. Median response time: 523ms (p95: 1645ms).
Last tested: 3/27/2026, 2:55:54 PM
“File hosting and static site management were disappointingly ineffective, with none of my ten attempts succeeding and a troubling average latency of 674ms. The frequent server errors, especially the HTTP 500 on the custom domain activation check, indicate a lack of reliability and care that makes it hard to recommend this service for any serious projects.”
“Every single operation failed—domain assignment, file uploads across different chains and sizes, site creation, everything returned errors or 500s. For a paid service handling file hosting and blockchain uploads, 0/10 success rate makes this completely unusable regardless of the 674ms average latency.”
Real requests we sent and the responses we received.
Create site with minimal 2-character name
POST /api/siteedge340msHTTP 400
Upload a medium 100MB file on Solana chain
POST /api/uploadtypical639msHTTP 400
Upload file with special characters and Unicode in filename
POST /api/uploadedge523msHTTP 400
Check custom domain activation status
GET /api/site/domain/statustypical1273msHTTP 500
POST /api/uploadGET /api/uploadsGET /api/download/:uploadIdPOST /api/sitePOST /api/site/renewPOST /api/site/activatePOST /api/site/domainGET /api/site/domain/status# StableUpload API Pay-per-upload file hosting and static site hosting. No API keys needed. ## Workflow: Single File Upload 1. `POST /api/upload` (paid) → `{ uploadId, uploadUrl, publicUrl }` 2. `curl -X PUT "$uploadUrl" -H "Content-Type: $contentType" --data-binary @file` 3. File is live at `publicUrl` for 6 months ## Workflow: Static Site Hosting 1. `POST /api/site` (paid) → `{ uploadId, uploadUrl }` 2. `curl -X PUT "$uploadUrl" -H "Content-Type: application/zip" --data-binary @site.zip` 3. `POST /api/site/activate` (SIWX auth, free) → `{ siteUrl, fileCount, files }` 4. Site is live at `siteUrl` (e.g. `https://{uploadId}.s.stableupload.dev/`) ### Update an existing site (free, no new payment) 1. `PUT /api/site` (SIWX auth) `{ "uploadId": "..." }` → `{ uploadUrl }` 2. Upload new zip to `uploadUrl` 3. `POST /api/site/activate` → extracts new zip, replaces old files ### Custom domain 1. Activate site first (steps above) 2. `POST /api/site/domain` (SIWX auth) `{ "uploadId": "...", "hostname": "www.example.com" }` → `{ dnsRecords }` 3. Create **both** DNS records from `dnsRecords[]`. If using StableDomains: ``` POST https://stabledomains.dev/api/domain/dns {"domain": "example.com", "action": "upsert", "records": [ {"type": "CNAME", "name": "www.example.com", "value": "s.stableupload.dev"}, {"type": "CNAME", "name": "_acme-challenge.www.example.com", "value": "www.example.com.1cab28587aab57d6.dcv.cloudflare.com"} ]} ``` DCV delegation format: `_acme-challenge.{hostname}` → `{hostname}.1cab28587aab57d6.dcv.cloudflare.com` The second record (DCV delegation) enables instant SSL (~30s vs minutes). 4. `GET /api/site/domain/status?uploadId=...` (SIWX auth) — poll until `ssl` is `"active"` Use `www.example.com` as the hostname. The API automatically sets up an apex redirect so `example.com` → `www.example.com` (via S3 redirect bucket + Route53 ALIAS record). The domain's hosted zone must be on the same AWS account. ### Renew a site `POST /api/site/renew` (paid) `{ "uploadId": "...", "count": 4 }` → extends 4 × 6 months. Price = tier price × count. ### Detach a custom domain `DELETE /api/site/domain` (SIWX auth) `{ "uploadId": "..." }` → `{ success, hostname }` Same-wallet reassignment: if you POST a hostname that's already attached to another site you own, it auto-detaches from the old site first. ### Custom response headers (_headers file) Include a `_headers` file in your zip to set custom response headers (Cloudflare Pages convention): ``` /* Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: credentialless /assets/* Cache-Control: public, max-age=31536000, immutable ``` This is required for WASM apps that need SharedArrayBuffer (COOP/COEP headers). ## Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | /api/upload | paid | Buy file upload slot | | POST | /api/site | paid | Buy site upload slot | | PUT | /api/site | SIWX | Get new upload URL for existing site | | POST | /api/site/renew | paid | Extend site expiration | | POST | /api/site/activate | SIWX | Extract zip and make site live | | POST | /api/site/domain | SIWX | Attach custom domain | | DELETE | /api/site/domain | SIWX | Detach custom domain | | GET | /api/site/domain/status | SIWX | Check domain TLS status | | GET | /api/uploads | SIWX | List all uploads | | GET | /api/download/:id | SIWX | Get upload details | ## Tiers (apply to both upload and site) - `10mb` → $0.02 (max 10 MB) - `100mb` → $0.20 (max 100 MB) - `1gb` → $2.00 (max 1 GB) **For sites, the tier limit applies to uncompressed size**, not the zip file size. Max 500 files per site. Uploads expire after 6 months by default. Use `POST /api/site/renew` to extend. ## POST /api/upload **Input:** `{ "filename": "photo.png", "contentType": "image/png", "tier": "10mb" }` **Output:** `{ "uploadId", "uploadUrl", "publicUrl", "siteUrl", "expiresAt", "maxSize", "curlExample" }` ## POST /api/site **Input:** `{ "filename": "my-site.zip", "tier": "100mb" }` **Output:** `{ "uploadId", "uploadUrl", "expiresAt", "maxSize", "curlExample" }` ## PUT /api/site **Input:** `{ "uploadId": "k7gm3nqp2" }` **Output:** `{ "uploadId", "uploadUrl", "expiresAt", "maxSize", "curlExample" }` ## POST /api/site/renew **Input:** `{ "uploadId": "k7gm3nqp2", "tier": "100mb", "count": 4 }` tier must match the upload's original tier. count defaults to 1. Each count extends by 6 months. Price = tier price × count. Expired sites cannot be renewed (files are deleted on expiry). **Output:** `{ "uploadId", "newExpiresAt", "count" }` ## POST /api/site/activate **Input:** `{ "uploadId": "k7gm3nqp2" }` **Output:** `{ "siteUrl", "fileCount", "files" }` ## POST /api/site/domain **Input:** `{ "uploadId": "k7gm3nqp2", "hostname": "www.coolsite.com" }` **Output:** ```json { "status": "provisioning", "hostname": "www.coolsite.com", "customHostnameId": "abc123...", "dnsRecords": [ {"type": "CNAME", "name": "www.coolsite.com", "value": "s.stableupload.dev"}, {"type": "CNAME", "name": "_acme-challenge.www.coolsite.com", "value": "www.coolsite.com.1cab28587aab57d6.dcv.cloudflare.com"} ] } ``` For fastest SSL (~30s), create DNS records BEFORE calling this endpoint (see Custom domain workflow above). ## DELETE /api/site/domain **Input:** `{ "uploadId": "k7gm3nqp2" }` **Output:** `{ "success": true, "hostname": "www.coolsite.com" }` ## GET /api/site/domain/status **Query:** `?uploadId=k7gm3nqp2` **Output:** `{ "status", "hostname", "ssl" }` ## GET /api/uploads (SIWX auth) List uploads for the authenticated wallet. Paginated. **Query params:** `limit` (default 50, max 100), `cursor` (from previous `nextCursor`) **Output:** `{ "uploads": [...], "nextCursor": "abc123" }` ## GET /api/download/:uploadId (SIWX auth) Get details for a specific upload including its public URL. ## Notes - Public URLs are permanent for the upload's lifetime (6 months, extendable via renew) - Files are stored on AWS S3; upload URLs expire after 1 hour - Site URLs: `https://{uploadId}.s.stableupload.dev/` - Custom domains get automatic HTTPS via Cloudflare DV cert (~30s with DCV delegation) - Zip sites: max 500 files, total size must fit within tier limit
https://stableupload.dev<a href="https://mpprimo.com/service/a39d4828-58e2-46ee-a340-7e7a7dab23e4"><img src="https://mpprimo.com/api/badge/a39d4828-58e2-46ee-a340-7e7a7dab23e4" alt="MPPrimo rating"></a>