--- name: chorus.host description: Publish static sites and deploy serverless workers to chorus.host via REST API. Use when a user asks to publish a site, host files, deploy a frontend, upload a static build, make a folder public, share a preview URL, host this build, or get a public URL for HTML/CSS/JS/assets. Trigger on any request to put files on the internet, publish to chorus.host, make something live, or deploy to the web. --- # chorus.host — Instant Web Hosting API > **Base URL:** `https://chorus.host` > **Docs:** `https://chorus.host/docs` > **LLM-readable docs:** `https://chorus.host/llms.txt` chorus.host provides two services: 1. **Static Sites** — publish HTML/CSS/JS files to `slug.chorus.host` 2. **Workers** — deploy serverless JavaScript/TypeScript to `slug.worker.chorus.host` ## Quick Start: Publish a Single-File Site No account needed. This is the minimum happy path: ```bash # 1. Compute SHA-256 hash of your file HASH="sha256:$(shasum -a 256 index.html | cut -d' ' -f1)" SIZE=$(wc -c < index.html | tr -d ' ') # 2. Create site — returns presigned upload URL curl -s -X POST https://chorus.host/v1/sites \ -H "Content-Type: application/json" \ -d "{\"files\":[{\"path\":\"index.html\",\"size\":$SIZE,\"contentType\":\"text/html\",\"hash\":\"$HASH\"}]}" \ > response.json # Save the claim token — you need it to manage this site later CLAIM_TOKEN=$(jq -r '.claimToken' response.json) UPLOAD_URL=$(jq -r '.uploads.pending[0].uploadUrl' response.json) FINALIZE_URL=$(jq -r '.version.finalizeUrl' response.json) SITE_URL=$(jq -r '.site.url' response.json) # 3. Upload file to presigned URL curl -s -X PUT "$UPLOAD_URL" \ -H "Content-Type: text/html" \ --data-binary @index.html # 4. Finalize — site goes live curl -s -X POST "https://chorus.host$FINALIZE_URL" \ -H "X-Claim-Token: $CLAIM_TOKEN" echo "Live at: $SITE_URL" ``` **Important:** Anonymous sites expire in 24 hours. Save the `claimToken` — send it as `X-Claim-Token` header to manage/update/finalize the site. To keep it permanently, create an account and claim it. --- ## Authentication | Mode | Header | Use case | |------|--------|----------| | Anonymous | (none) on create; `X-Claim-Token: ctk_...` on subsequent requests | Quick publish, 24h expiry | | API Key | `Authorization: Bearer chk_...` | Permanent sites, workers, full management | | Claim Token | `X-Claim-Token: ctk_...` | Manage anonymous sites before claiming | **Auth rules for site management endpoints (update, finalize, delete, etc.):** | Site state | Required header | |------------|-----------------| | Owned (has account) | `Authorization: Bearer chk_...` (must be the owner) | | Anonymous/unclaimed | `X-Claim-Token: ctk_...` (from create response) | | Claiming an anonymous site | Both: `Authorization: Bearer chk_...` + `claimToken` in JSON body | ### Get an API key ``` POST /v1/auth/send-otp Content-Type: application/json {"email": "you@example.com"} ``` ``` POST /v1/auth/verify-otp Content-Type: application/json {"email": "you@example.com", "code": "123456"} → {"apiKey": "chk_..."} ``` --- ## Static Sites API ### Create a site ``` POST /v1/sites Authorization: Bearer (optional — omit for anonymous) Idempotency-Key: (optional — prevents duplicate creates on retry) Content-Type: application/json { "slug": "my-site", "files": [ { "path": "index.html", "size": 1024, "contentType": "text/html", "hash": "sha256:<64 lowercase hex chars>" } ] } ``` - `slug` is optional (auto-generated if omitted) - `hash` is the SHA-256 hex digest of the file contents, prefixed with `sha256:` - Files with matching hashes across any site are deduplicated (skip upload) Response: ```json { "site": { "id": "01JQXYZ...", "slug": "my-site", "url": "https://my-site.chorus.host", "expiresAt": "2026-03-21T15:04:05Z", "createdAt": "2026-03-20T15:04:05Z" }, "version": { "id": "01JQABC...", "finalizeUrl": "/v1/sites/my-site/versions/01JQABC.../finalize" }, "uploads": { "pending": [ { "path": "index.html", "uploadUrl": "https://r2.cloudflarestorage.com/...", "uploadMethod": "put" } ], "skipped": [] }, "claimToken": "ctk_...", "claimUrl": "https://chorus.host/claim/my-site#ctk_..." } ``` `claimToken` and `claimUrl` are only returned for anonymous creates. `expiresAt` is null for authenticated creates. ### Upload files PUT each file to its presigned URL with the correct Content-Type: ``` PUT Content-Type: text/html ``` Upload URLs expire after 1 hour. Use the refresh endpoint to get new ones if needed. ### Finalize a version ``` POST /v1/sites/:slug/versions/:versionId/finalize Authorization: Bearer ``` Or for anonymous sites: ``` POST /v1/sites/:slug/versions/:versionId/finalize X-Claim-Token: ctk_... ``` Site is live immediately after finalize succeeds. ### Create a new version (update existing site) ``` POST /v1/sites/:slug/versions Authorization: Bearer Content-Type: application/json {"files": [...]} ``` Same flow: upload files, then finalize. ### List sites ``` GET /v1/sites?limit=20&offset=0 Authorization: Bearer ``` Pagination: `limit` (1-100, default 20), `offset` (default 0). ### Update metadata ``` PATCH /v1/sites/:slug/metadata Authorization: Bearer Content-Type: application/json { "title": "My Site", "description": "A demo", "ogImagePath": "preview.png", "expiresAt": null } ``` ### Set password protection ``` PUT /v1/sites/:slug/password Authorization: Bearer Content-Type: application/json {"username": "demo", "password": "s3cret"} ``` Password max length: 72 characters (bcrypt limit). ### Remove password protection ``` DELETE /v1/sites/:slug/password Authorization: Bearer ``` ### Delete a site ``` DELETE /v1/sites/:slug Authorization: Bearer ``` Returns `204 No Content` on success. ### Claim an anonymous site Requires both Bearer auth (to identify the account) and the claim token: ``` POST /v1/sites/:slug/claim Authorization: Bearer Content-Type: application/json {"claimToken": "ctk_..."} ``` ### List versions ``` GET /v1/sites/:slug/versions?limit=20&offset=0 Authorization: Bearer ``` ### Rollback to a version ``` POST /v1/sites/:slug/versions/:versionId/rollback Authorization: Bearer ``` ### Refresh upload URLs If presigned URLs have expired (after 1 hour): ``` POST /v1/sites/:slug/versions/:versionId/uploads/refresh Authorization: Bearer Content-Type: application/json {"paths": ["index.html"]} ``` ### Complete multipart upload For files >= 250 MB that use multipart upload: ``` POST /v1/sites/:slug/versions/:versionId/uploads/complete Authorization: Bearer Content-Type: application/json { "path": "large-file.mp4", "uploadId": "mpu_abc123", "parts": [ {"partNumber": 1, "etag": "\"abc123...\""}, {"partNumber": 2, "etag": "\"def456...\""} ] } ``` --- ## Workers API Workers are serverless JavaScript/TypeScript functions on Cloudflare's edge network. Ideal as API backends for static sites. All worker endpoints require `Authorization: Bearer `. **Note:** If workers are not configured on the server, deploy, delete worker, set secret, delete secret, rollback, and logs/tail return `503 Service Unavailable`. Create, list workers, get worker, list secrets, and list deployments are DB-backed and work regardless. ### Create a worker ``` POST /v1/workers Authorization: Bearer Idempotency-Key: (optional — prevents duplicate creates on retry) Content-Type: application/json {"slug": "my-api"} ``` Response: ```json { "slug": "my-api", "url": "https://my-api.worker.chorus.host", "createdAt": "2026-03-20T15:04:05Z" } ``` ### List workers ``` GET /v1/workers Authorization: Bearer ``` ### Get worker details ``` GET /v1/workers/:slug Authorization: Bearer ``` Response: ```json { "slug": "my-api", "url": "https://my-api.worker.chorus.host", "currentDeployment": { "id": "01JQDEP...", "version": 3, "entryPoint": "index.ts", "sizeBytes": 1234, "deployedAt": "2026-03-20T15:10:00Z" }, "createdAt": "2026-03-20T15:04:05Z" } ``` ### Deploy a worker Uses `multipart/form-data` with a `metadata` JSON field and one or more `file` fields. ``` POST /v1/workers/:slug/deploy Authorization: Bearer Content-Type: multipart/form-data Form fields: metadata = {"entryPoint": "index.ts", "compatibilityDate": "2024-09-23", "compatibilityFlags": ["nodejs_compat"]} file = @index.ts file = @lib/utils.ts ``` curl example: ```bash curl -X POST https://chorus.host/v1/workers/my-api/deploy \ -H "Authorization: Bearer your-api-key" \ -F 'metadata={"entryPoint":"index.ts","compatibilityDate":"2024-09-23","compatibilityFlags":["nodejs_compat"]}' \ -F "file=@index.ts" ``` Metadata fields: - `entryPoint` (required) — main module filename - `compatibilityDate` (required) — Cloudflare compatibility date - `compatibilityFlags` — e.g. `["nodejs_compat"]` - `crons` — cron expressions, e.g. `["0 * * * *"]` - `kvNamespaces` — `[{"bindingName": "CACHE"}]` - `durableObjects` — `[{"className": "Room", "bindingName": "ROOMS"}]` Response: ```json { "slug": "my-api", "url": "https://my-api.worker.chorus.host", "deploymentId": "01JQDEP...", "version": 3, "sizeBytes": 1234, "fileCount": 2, "entryPoint": "index.ts", "deployedAt": "2026-03-20T15:10:00Z" } ``` Deploy is idempotent — if the script hash matches the current deployment, the existing deployment is returned. ### Delete a worker ``` DELETE /v1/workers/:slug Authorization: Bearer ``` If the worker uses Durable Objects, you must confirm: ``` DELETE /v1/workers/:slug Authorization: Bearer Content-Type: application/json {"confirmDoDeletion": true} ``` Returns `204 No Content`. ### Set a secret ``` PUT /v1/workers/:slug/secrets/:name Authorization: Bearer Content-Type: application/json {"value": "postgres://user:pass@host/db"} ``` Secret name must match `[A-Z][A-Z0-9_]{0,63}`. Value max size: 5 KB. Secrets are encrypted by Cloudflare and take effect immediately. Response: ```json {"name": "DATABASE_URL", "createdAt": "2026-03-20T15:04:05Z"} ``` ### Delete a secret ``` DELETE /v1/workers/:slug/secrets/:name Authorization: Bearer ``` Returns `204 No Content`. ### List secret names ``` GET /v1/workers/:slug/secrets Authorization: Bearer ``` Returns names and creation dates (values are never exposed): ```json { "data": [ {"name": "DATABASE_URL", "createdAt": "2026-03-20T15:04:05Z"}, {"name": "API_KEY", "createdAt": "2026-03-20T15:04:05Z"} ], "totalCount": 2, "limit": 20, "offset": 0 } ``` ### List deployment history ``` GET /v1/workers/:slug/deployments Authorization: Bearer ``` ### Rollback to previous deployment ``` POST /v1/workers/:slug/rollback Authorization: Bearer Content-Type: application/json {"deploymentId": "01JQDEP..."} ``` `deploymentId` is optional — defaults to the previous deployment. Response includes `rolledBackFrom` field: ```json { "slug": "my-api", "url": "https://my-api.worker.chorus.host", "deploymentId": "01JQNEW...", "version": 4, "sizeBytes": 1234, "fileCount": 2, "entryPoint": "index.ts", "deployedAt": "2026-03-20T16:00:00Z", "rolledBackFrom": "01JQDEP..." } ``` ### Real-time logs ``` GET /v1/workers/:slug/logs/tail Authorization: Bearer ``` Returns a WebSocket URL for live log streaming: ```json { "url": "wss://tail.developers.workers.dev/..." } ``` --- ## Example: Full Publish Flow (Node.js) ```javascript const crypto = require('crypto'); const fs = require('fs'); const BASE = 'https://chorus.host'; // 1. Compute file hash const content = fs.readFileSync('index.html'); const hash = 'sha256:' + crypto.createHash('sha256').update(content).digest('hex'); // 2. Create site (anonymous — no auth needed) const createRes = await fetch(`${BASE}/v1/sites`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ files: [{ path: 'index.html', size: content.length, contentType: 'text/html', hash }] }) }); const { site, version, uploads, claimToken } = await createRes.json(); // IMPORTANT: save claimToken — you need it for all subsequent requests // 3. Upload files for (const upload of uploads.pending) { const fileContent = fs.readFileSync(upload.path); await fetch(upload.uploadUrl, { method: 'PUT', body: fileContent }); } // 4. Finalize (use claim token for anonymous sites) await fetch(`${BASE}${version.finalizeUrl}`, { method: 'POST', headers: { 'X-Claim-Token': claimToken } }); console.log(`Live at: ${site.url}`); ``` ## Example: Hono Backend Worker ```typescript import { Hono } from 'hono' const app = new Hono() app.get('/api/hello', (c) => { return c.json({ message: 'Hello from chorus.host!' }) }) app.post('/api/contact', async (c) => { const body = await c.req.json() return c.json({ ok: true }) }) export default app ``` Deploy: `my-app.chorus.host` serves the frontend, `my-app.worker.chorus.host` serves the API. --- ## Limits | Limit | Anonymous | Authenticated | |-------|-----------|---------------| | Site lifetime | 24 hours | Permanent | | Max file size | 250 MB | 5 GB | | Total site size | 10 GB | 10 GB | | Publish rate | 5/hour per IP | 60/hour per key | | Worker deploy rate | N/A | 20/hour per key | | Max files per version | 10,000 | 10,000 | | Worker script bundle | N/A | 3 MB compressed | | Max worker files | N/A | 100 | | Secret value size | N/A | 5 KB | | Upload URL expiry | 1 hour | 1 hour | | Pagination max limit | 100 | 100 | ## Error Format All errors return JSON: ```json {"error": "slug is already taken"} ``` Common status codes: 400 (bad request), 401 (unauthorized), 404 (not found), 409 (conflict/idempotency mismatch), 413 (too large), 422 (unprocessable), 429 (rate limited), 502 (Cloudflare API failure), 503 (workers not configured). ## Endpoint Reference | Method | Path | Auth | Description | |--------|------|------|-------------| | POST | `/v1/auth/send-otp` | None | Send OTP code to email | | POST | `/v1/auth/verify-otp` | None | Verify OTP, get API key | | POST | `/v1/sites` | Optional | Create site with file manifest | | GET | `/v1/sites` | Bearer | List your sites | | PATCH | `/v1/sites/:slug/metadata` | Bearer or Claim | Update title, description, OG image | | PUT | `/v1/sites/:slug/password` | Bearer or Claim | Set password protection | | DELETE | `/v1/sites/:slug/password` | Bearer or Claim | Remove password protection | | DELETE | `/v1/sites/:slug` | Bearer or Claim | Delete site (204) | | POST | `/v1/sites/:slug/claim` | Bearer + Claim body | Claim anonymous site | | POST | `/v1/sites/:slug/versions` | Bearer or Claim | Create new version | | GET | `/v1/sites/:slug/versions` | Bearer or Claim | List versions | | POST | `/v1/sites/:slug/versions/:id/finalize` | Bearer or Claim | Verify uploads, go live | | POST | `/v1/sites/:slug/versions/:id/uploads/refresh` | Bearer or Claim | Refresh presigned URLs | | POST | `/v1/sites/:slug/versions/:id/uploads/complete` | Bearer or Claim | Complete multipart upload | | POST | `/v1/sites/:slug/versions/:id/rollback` | Bearer or Claim | Rollback to this version | | POST | `/v1/workers` | Bearer | Create worker | | GET | `/v1/workers` | Bearer | List workers | | GET | `/v1/workers/:slug` | Bearer | Get worker details | | POST | `/v1/workers/:slug/deploy` | Bearer | Deploy worker (3 MB bundle limit) | | DELETE | `/v1/workers/:slug` | Bearer | Delete worker (204) | | PUT | `/v1/workers/:slug/secrets/:name` | Bearer | Set secret | | DELETE | `/v1/workers/:slug/secrets/:name` | Bearer | Delete secret (204) | | GET | `/v1/workers/:slug/secrets` | Bearer | List secret names | | GET | `/v1/workers/:slug/deployments` | Bearer | List deployments | | POST | `/v1/workers/:slug/rollback` | Bearer | Rollback worker | | GET | `/v1/workers/:slug/logs/tail` | Bearer | Get log stream URL |