docs

chorus.host API

Free, instant static file hosting for AI agents. Publish any file or folder to the web in seconds.

chorus.host is a static hosting service designed to be called by AI agents. There is no dashboard, no CLI, no GUI — just a simple HTTP API. Your agent sends a file manifest, uploads files to presigned URLs, and gets back a live URL at slug.chorus.host.

Every site gets its own subdomain. Updates are atomic and versioned. No account is required for one-off publishes (sites expire in 24 hours), or sign in for permanent hosting.

Base URL: https://chorus.host

Quick Start

Publishing a site takes three API calls:

Step 1: Create a site with your file manifest

# Create a site with one HTML file
curl -X POST https://chorus.host/v1/sites \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "path": "index.html",
        "size": 1024,
        "contentType": "text/html",
        "hash": "sha256:a1b2c3d4e5f6...64 hex chars"
      }
    ]
  }'

The response includes presigned upload URLs and a finalize URL:

{
  "site": {
    "id": "site_abc123",
    "slug": "autumn-river-42",
    "url": "https://autumn-river-42.chorus.host",
    "expiresAt": "2026-03-20T15:04:05Z",
    "createdAt": "2026-03-19T15:04:05Z"
  },
  "version": {
    "id": "ver_xyz789",
    "finalizeUrl": "/v1/sites/autumn-river-42/versions/ver_xyz789/finalize"
  },
  "uploads": {
    "pending": [
      {
        "path": "index.html",
        "uploadUrl": "https://storage.example.com/presigned-url...",
        "uploadMethod": "put"
      }
    ],
    "skipped": []
  },
  "claimToken": "ctk_abc123...",
  "claimUrl": "https://chorus.host/claim/autumn-river-42#ctk_abc123..."
}

Step 2: Upload each file to its presigned URL

curl -X PUT "https://storage.example.com/presigned-url..." \
  -H "Content-Type: text/html" \
  --data-binary @index.html

Step 3: Finalize the version to go live

curl -X POST https://chorus.host/v1/sites/autumn-river-42/versions/ver_xyz789/finalize

Response:

{
  "site": {
    "id": "site_abc123",
    "slug": "autumn-river-42",
    "url": "https://autumn-river-42.chorus.host",
    "currentVersionId": "ver_xyz789",
    "expiresAt": "2026-03-20T15:04:05Z",
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:04:10Z"
  },
  "version": {
    "id": "ver_xyz789",
    "status": "live",
    "fileCount": 1,
    "totalSize": 1024,
    "finalizedAt": "2026-03-19T15:04:10Z",
    "createdAt": "2026-03-19T15:04:05Z"
  }
}

Your site is now live at https://autumn-river-42.chorus.host.

Authentication

chorus.host supports two modes of operation:

Anonymous (no auth required)

Just call the API with no credentials. Your site will be assigned a random slug and will expire in 24 hours. You receive a claimToken and claimUrl in the response that you can use to claim ownership later.

To manage an anonymous site (update it, delete it, etc.), include the claim token in the X-Claim-Token header:

curl -X PATCH https://chorus.host/v1/sites/autumn-river-42/metadata \
  -H "X-Claim-Token: ctk_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"title": "My Site"}'

Authenticated (Bearer token)

Include your API key in the Authorization header. Authenticated sites are permanent (no expiry unless you set one). You get higher rate limits and larger file size allowances.

curl -X POST https://chorus.host/v1/sites \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"files": [...]}'
FeatureAnonymousAuthenticated
Site lifetime24 hoursPermanent
Max file size250 MB5 GB
Publish rate limit5/hour per IP60/hour per key
Custom slugAllowedAllowed
Auth headerNone (or X-Claim-Token)Authorization: Bearer <key>

Publishing

Create a site

POST /v1/sites

Creates a new site and its first version. Returns presigned upload URLs for each file in your manifest.

Request body

FieldTypeRequiredDescription
filesarrayYesArray of file manifest entries
files[].pathstringYesFile path relative to site root (e.g. index.html, assets/style.css)
files[].sizeintegerYesFile size in bytes
files[].contentTypestringYesMIME type (e.g. text/html)
files[].hashstringYesSHA-256 hash in format sha256:<64 hex chars>
slugstringNoCustom slug for the subdomain. Auto-generated if omitted.
expiresAtstringNoRFC 3339 timestamp. Only for authenticated requests.

Headers

HeaderRequiredDescription
AuthorizationNoBearer <api-key> for authenticated requests
Idempotency-KeyNoUnique key to safely retry the request. If a site was already created with this key, the original response is replayed.
curl -X POST https://chorus.host/v1/sites \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer your-api-key" \
  -d '{
    "slug": "my-project",
    "files": [
      {
        "path": "index.html",
        "size": 2048,
        "contentType": "text/html",
        "hash": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
      },
      {
        "path": "style.css",
        "size": 512,
        "contentType": "text/css",
        "hash": "sha256:d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"
      }
    ]
  }'

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "my-project",
    "url": "https://my-project.chorus.host",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:04:05Z"
  },
  "version": {
    "id": "ver_xyz789",
    "finalizeUrl": "/v1/sites/my-project/versions/ver_xyz789/finalize"
  },
  "uploads": {
    "pending": [
      {
        "path": "index.html",
        "uploadUrl": "https://storage.example.com/presigned-url-1...",
        "uploadMethod": "put"
      },
      {
        "path": "style.css",
        "uploadUrl": "https://storage.example.com/presigned-url-2...",
        "uploadMethod": "put"
      }
    ],
    "skipped": []
  }
}

Anonymous responses also include claimToken and claimUrl fields that let you claim ownership of the site later.

Upload files

Upload each file to its presigned URL using a PUT request. The Content-Type header must match the contentType from the manifest.

curl -X PUT "https://storage.example.com/presigned-url-1..." \
  -H "Content-Type: text/html" \
  --data-binary @index.html

Upload URLs expire after 1 hour. If they expire before you finish uploading, use the refresh uploads endpoint to get new ones.

Finalize a version

POST /v1/sites/:slug/versions/:versionId/finalize

Verifies all uploaded files match their declared hashes and sizes, then atomically makes the version live. The site is accessible immediately after a successful finalize.

curl -X POST https://chorus.host/v1/sites/my-project/versions/ver_xyz789/finalize \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "my-project",
    "url": "https://my-project.chorus.host",
    "currentVersionId": "ver_xyz789",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:04:10Z"
  },
  "version": {
    "id": "ver_xyz789",
    "status": "live",
    "fileCount": 2,
    "totalSize": 2560,
    "finalizedAt": "2026-03-19T15:04:10Z",
    "createdAt": "2026-03-19T15:04:05Z"
  }
}

Multipart uploads

Files 250 MB and larger are automatically assigned multipart uploads. Instead of a single uploadUrl, you receive an uploadId and an array of partUrls.

// In the create site response, large files look like:
{
  "path": "large-video.mp4",
  "uploadMethod": "multipart",
  "uploadId": "mpu_abc123",
  "partUrls": [
    "https://storage.example.com/part-1-url...",
    "https://storage.example.com/part-2-url..."
  ]
}

Upload each part in order (8 MB per part), then call the complete multipart endpoint with the ETags returned by each part upload. After all multipart uploads are completed, call finalize as usual.

Content deduplication

chorus.host deduplicates files by their SHA-256 hash. If a file with the same hash was uploaded in a previous finalized version (on any site), it will appear in the skipped array instead of pending:

{
  "skipped": [
    {
      "path": "logo.png",
      "reason": "hash_match"
    }
  ]
}

You do not need to upload skipped files. They are already stored and will be served correctly.

Site Management

List sites

GET /v1/sites

Returns all sites owned by the authenticated user. Requires Bearer auth.

Query parameters

ParameterTypeDefaultDescription
limitinteger50Max results to return
offsetinteger0Pagination offset
curl https://chorus.host/v1/sites?limit=10 \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "sites": [
    {
      "id": "site_abc123",
      "slug": "my-project",
      "url": "https://my-project.chorus.host",
      "currentVersionId": "ver_xyz789",
      "ownerId": "usr_def456",
      "title": "My Project",
      "expiresAt": null,
      "createdAt": "2026-03-19T15:04:05Z",
      "updatedAt": "2026-03-19T15:04:10Z"
    }
  ],
  "totalCount": 1,
  "limit": 10,
  "offset": 0
}

Update metadata

PATCH /v1/sites/:slug/metadata

Update site metadata. All fields are optional — only included fields are updated. Set a field to null to clear it.

Request body

FieldTypeDescription
titlestring | nullSite title (used in Open Graph tags)
descriptionstring | nullSite description
ogImagePathstring | nullPath to an image file in the site for Open Graph
expiresAtstring | nullRFC 3339 expiry. Set to null to make permanent (authenticated only).
curl -X PATCH https://chorus.host/v1/sites/my-project/metadata \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Project",
    "description": "A demo site published by my agent",
    "ogImagePath": "preview.png"
  }'

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "my-project",
    "url": "https://my-project.chorus.host",
    "currentVersionId": "ver_xyz789",
    "title": "My Project",
    "description": "A demo site published by my agent",
    "ogImagePath": "preview.png",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:05:00Z"
  }
}

Set password protection

PUT /v1/sites/:slug/password

Protects the site with HTTP Basic authentication. Visitors will be prompted for credentials before seeing the site.

curl -X PUT https://chorus.host/v1/sites/my-project/password \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "demo",
    "password": "s3cret"
  }'

Response 200 OK:

{
  "success": true
}

Remove password protection

DELETE /v1/sites/:slug/password
curl -X DELETE https://chorus.host/v1/sites/my-project/password \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "success": true
}

Delete a site

DELETE /v1/sites/:slug

Permanently deletes a site and all its versions. This action cannot be undone.

curl -X DELETE https://chorus.host/v1/sites/my-project \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "success": true
}

Claim an anonymous site

POST /v1/sites/:slug/claim

Transfer ownership of an anonymous site to your account. Requires both Bearer auth (to identify the new owner) and the claim token (to prove you created the site). Claiming a site removes its expiry and makes it permanent.

curl -X POST https://chorus.host/v1/sites/autumn-river-42/claim \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"claimToken": "ctk_abc123..."}'

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "autumn-river-42",
    "url": "https://autumn-river-42.chorus.host",
    "currentVersionId": "ver_xyz789",
    "ownerId": "usr_def456",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:10:00Z"
  }
}

Versions

Every publish creates a new version. Versions are immutable once finalized. You can update a site by creating a new version, or roll back to any previous version.

Create a new version

POST /v1/sites/:slug/versions

Creates a new version for an existing site. Works exactly like creating a site, but attaches to the existing slug. The new version does not go live until you finalize it.

curl -X POST https://chorus.host/v1/sites/my-project/versions \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      {
        "path": "index.html",
        "size": 3072,
        "contentType": "text/html",
        "hash": "sha256:abc123..."
      }
    ]
  }'

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "my-project",
    "url": "https://my-project.chorus.host",
    "currentVersionId": "ver_xyz789",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T15:04:10Z"
  },
  "version": {
    "id": "ver_new456",
    "finalizeUrl": "/v1/sites/my-project/versions/ver_new456/finalize"
  },
  "uploads": {
    "pending": [
      {
        "path": "index.html",
        "uploadUrl": "https://storage.example.com/presigned-url...",
        "uploadMethod": "put"
      }
    ],
    "skipped": []
  }
}

List versions

GET /v1/sites/:slug/versions

Returns all versions for a site, ordered by creation date (newest first).

Query parameters

ParameterTypeDefaultDescription
limitinteger20Max results to return
offsetinteger0Pagination offset
curl https://chorus.host/v1/sites/my-project/versions \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "versions": [
    {
      "id": "ver_xyz789",
      "status": "live",
      "fileCount": 2,
      "totalSize": 2560,
      "finalizedAt": "2026-03-19T15:04:10Z",
      "createdAt": "2026-03-19T15:04:05Z"
    },
    {
      "id": "ver_old123",
      "status": "superseded",
      "fileCount": 1,
      "totalSize": 1024,
      "finalizedAt": "2026-03-18T10:00:00Z",
      "createdAt": "2026-03-18T09:59:55Z"
    }
  ],
  "totalCount": 2,
  "limit": 20,
  "offset": 0
}

Rollback to a version

POST /v1/sites/:slug/versions/:versionId/rollback

Instantly redeploys a previous version. Creates a new version by copying the file manifest from the target version, then atomically makes it live. The target version must be finalized (not pending).

curl -X POST https://chorus.host/v1/sites/my-project/versions/ver_old123/rollback \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "site": {
    "id": "site_abc123",
    "slug": "my-project",
    "url": "https://my-project.chorus.host",
    "currentVersionId": "ver_rolled789",
    "expiresAt": null,
    "createdAt": "2026-03-19T15:04:05Z",
    "updatedAt": "2026-03-19T16:00:00Z"
  },
  "version": {
    "id": "ver_rolled789",
    "status": "live",
    "fileCount": 1,
    "totalSize": 1024,
    "finalizedAt": "2026-03-19T16:00:00Z",
    "createdAt": "2026-03-19T16:00:00Z"
  }
}

Refresh upload URLs

POST /v1/sites/:slug/versions/:versionId/uploads/refresh

Regenerates expired presigned upload URLs for a pending version. Optionally pass a list of paths to refresh only specific files.

Request body (optional)

FieldTypeDescription
pathsstring[]Optional list of file paths to refresh. Omit to refresh all.
curl -X POST https://chorus.host/v1/sites/my-project/versions/ver_xyz789/uploads/refresh \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"paths": ["index.html"]}'

Response 200 OK:

{
  "uploads": {
    "pending": [
      {
        "path": "index.html",
        "uploadUrl": "https://storage.example.com/new-presigned-url...",
        "uploadMethod": "put"
      }
    ],
    "skipped": []
  }
}

Complete multipart upload

POST /v1/sites/:slug/versions/:versionId/uploads/complete

Assembles a multipart upload after all parts have been uploaded. You must call this for each file that used multipart upload method before calling finalize.

Request body

FieldTypeRequiredDescription
pathstringYesFile path in the manifest
uploadIdstringYesThe upload ID from the create response
partsarrayYesArray of completed parts
parts[].partNumberintegerYesPart number (1-based)
parts[].etagstringYesETag returned by the part upload
curl -X POST https://chorus.host/v1/sites/my-project/versions/ver_xyz789/uploads/complete \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "path": "large-video.mp4",
    "uploadId": "mpu_abc123",
    "parts": [
      {"partNumber": 1, "etag": "\"abc123...\""},
      {"partNumber": 2, "etag": "\"def456...\""}
    ]
  }'

Response 200 OK:

{
  "success": true
}

Workers

Workers are serverless JavaScript/TypeScript functions that run on Cloudflare's edge network. They are the compute layer for chorus.host — deploy an API backend alongside your static site.

The recommended pattern is to pair a static frontend with a Worker backend:

# Static frontend
my-app.chorus.host           → HTML/CSS/JS (Beacon)

# API backend (Hono recommended)
my-app.worker.chorus.host    → Serverless Worker (Cloudflare edge)

Workers require authentication. Anonymous workers are not supported. You must include a Bearer token with all Workers API requests.

Workers share a global slug namespace with static sites — a slug is unique across both. TypeScript and JavaScript are both accepted (Cloudflare handles TS transpilation at the edge).

Create a worker

POST /v1/workers

Creates a new worker and reserves a slug. The worker is not live until you deploy code to it.

Request body

FieldTypeRequiredDescription
slugstringNoCustom slug for the subdomain. Auto-generated if omitted.

Headers

HeaderRequiredDescription
AuthorizationYesBearer <api-key>
Idempotency-KeyNoUnique key to safely retry the request
curl -X POST https://chorus.host/v1/workers \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"slug": "my-api"}'

Response 200 OK:

{
  "slug": "my-api",
  "url": "https://my-api.worker.chorus.host",
  "createdAt": "2026-03-20T15:04:05Z"
}

List workers

GET /v1/workers

Returns all workers owned by the authenticated user.

curl https://chorus.host/v1/workers \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "workers": [
    {
      "slug": "my-api",
      "url": "https://my-api.worker.chorus.host",
      "currentDeployment": {
        "id": "dep_abc123",
        "version": 3,
        "deployedAt": "2026-03-20T15:10:00Z"
      },
      "createdAt": "2026-03-20T15:04:05Z"
    }
  ]
}

Get worker details

GET /v1/workers/:slug

Returns detailed information about a worker, including its current deployment and bindings.

curl https://chorus.host/v1/workers/my-api \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "slug": "my-api",
  "url": "https://my-api.worker.chorus.host",
  "currentDeployment": {
    "id": "dep_abc123",
    "version": 3,
    "entryPoint": "index.ts",
    "sizeBytes": 1234,
    "deployedAt": "2026-03-20T15:10:00Z"
  },
  "bindings": {
    "secrets": ["DATABASE_URL", "API_KEY"],
    "kvNamespaces": [{"bindingName": "CACHE"}],
    "crons": ["0 * * * *"]
  },
  "createdAt": "2026-03-20T15:04:05Z"
}

Deploy a worker

POST /v1/workers/:slug/deploy

Uploads and activates a worker script in a single request. The request body is multipart/form-data with a JSON metadata part and one or more file parts.

Multipart form fields

Field nameTypeRequiredDescription
metadataJSON (form value)YesDeployment metadata (see below)
fileFile upload(s)YesOne or more script files. Use field name file for each.

Metadata fields

FieldTypeRequiredDescription
entryPointstringYesMain module filename (e.g. index.ts, index.js)
compatibilityDatestringNoCloudflare compatibility date (e.g. 2024-09-23)
compatibilityFlagsstring[]NoCompatibility flags (e.g. ["nodejs_compat"])
cronsstring[]NoCron trigger expressions (e.g. ["0 * * * *"])
kvNamespacesarrayNoKV namespace bindings ([{"bindingName": "CACHE"}])
durableObjectsarrayNoDurable Object bindings ([{"className": "Room", "bindingName": "ROOMS"}])
# Deploy a simple worker
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"
# Deploy multiple files
curl -X POST https://chorus.host/v1/workers/my-api/deploy \
  -H "Authorization: Bearer your-api-key" \
  -F 'metadata={"entryPoint":"index.ts"}' \
  -F "file=@index.ts" \
  -F "file=@lib/utils.ts"

Response 200 OK:

{
  "slug": "my-api",
  "url": "https://my-api.worker.chorus.host",
  "deploymentId": "dep_abc123",
  "version": 3,
  "sizeBytes": 1234,
  "fileCount": 2,
  "entryPoint": "index.ts",
  "deployedAt": "2026-03-20T15:10:00Z",
  "bindings": {
    "secrets": ["DATABASE_URL"],
    "kvNamespaces": [{"bindingName": "CACHE", "namespaceId": "ns_xyz"}]
  }
}

Deploy is idempotent. If the script hash matches the current deployment and bindings haven't changed, the existing deployment is returned without re-uploading to Cloudflare.

Here is an example Hono worker you can deploy as a backend for your static site:

// index.ts
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

Delete a worker

DELETE /v1/workers/:slug

Permanently deletes a worker, all its deployments, and removes the script from Cloudflare's edge. This action cannot be undone.

curl -X DELETE https://chorus.host/v1/workers/my-api \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "success": true
}

Secrets

Secrets are environment variables encrypted at rest by Cloudflare. Secret values are never stored in Beacon's database — they go straight to Cloudflare. Beacon only tracks the secret name.

Set a secret

PUT /v1/workers/:slug/secrets/:name

Sets or updates a secret. Secrets take effect immediately on the running worker (Cloudflare hot-reloads secrets without redeploying).

Request body

FieldTypeRequiredDescription
valuestringYesThe secret value
curl -X PUT https://chorus.host/v1/workers/my-api/secrets/DATABASE_URL \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"value": "postgres://user:pass@host/db"}'

Response 200 OK:

{
  "success": true
}

Delete a secret

DELETE /v1/workers/:slug/secrets/:name
curl -X DELETE https://chorus.host/v1/workers/my-api/secrets/DATABASE_URL \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "success": true
}

List secrets

GET /v1/workers/:slug/secrets

Returns the names of all secrets set on the worker. Values are never returned.

curl https://chorus.host/v1/workers/my-api/secrets \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "secrets": ["DATABASE_URL", "API_KEY"]
}

Deployment history

GET /v1/workers/:slug/deployments

Returns the deployment history for a worker, ordered by creation date (newest first).

curl https://chorus.host/v1/workers/my-api/deployments \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "deployments": [
    {
      "id": "dep_abc123",
      "version": 3,
      "entryPoint": "index.ts",
      "sizeBytes": 1234,
      "fileCount": 2,
      "status": "live",
      "deployedAt": "2026-03-20T15:10:00Z"
    },
    {
      "id": "dep_old789",
      "version": 2,
      "entryPoint": "index.ts",
      "sizeBytes": 980,
      "fileCount": 1,
      "status": "superseded",
      "deployedAt": "2026-03-19T12:00:00Z"
    }
  ]
}

Rollback

POST /v1/workers/:slug/rollback

Rolls back to a previous deployment. If no deploymentId is specified, rolls back to the immediately previous deployment. The previous script bundle is fetched from storage and re-deployed to Cloudflare's edge.

Request body (optional)

FieldTypeRequiredDescription
deploymentIdstringNoSpecific deployment ID to roll back to. Defaults to previous deployment.
curl -X POST https://chorus.host/v1/workers/my-api/rollback \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"deploymentId": "dep_old789"}'

Response 200 OK:

{
  "slug": "my-api",
  "url": "https://my-api.worker.chorus.host",
  "deploymentId": "dep_rolled456",
  "version": 4,
  "deployedAt": "2026-03-20T16:00:00Z"
}

Logs

GET /v1/workers/:slug/logs/tail

Returns a WebSocket URL for real-time log streaming. Connect to the returned URL to receive live console.log output, errors, and request metadata from your worker.

curl https://chorus.host/v1/workers/my-api/logs/tail \
  -H "Authorization: Bearer your-api-key"

Response 200 OK:

{
  "url": "wss://chorus.host/v1/workers/my-api/logs/tail/ws?token=...",
  "expiresAt": "2026-03-20T16:04:05Z"
}

Workers run on Cloudflare's edge, not on the Beacon server. If Beacon goes down, your deployed workers continue running. Only the management API (deploy, delete, secrets) is affected.

Serving

Once a site is finalized, it is served at https://<slug>.chorus.host. Files are stored on Cloudflare R2 and served from the global edge network.

URL resolution

When a request arrives, chorus.host resolves the file to serve using this chain (in order):

  1. Exact match/style.css serves the file at path style.css
  2. Trailing slash redirect/blog redirects (302) to /blog/ if files exist under that prefix
  3. Directory index/blog/ serves blog/index.html if it exists
  4. Single-file auto-viewer — If the site has exactly one file, serves an HTML wrapper with appropriate viewer (image, video, PDF, or download link)
  5. Directory listing — If the path is a directory with files, renders an HTML file listing
  6. 404 — If nothing matches

Cache headers

Content typeCache-Control
HTML files (.html, .htm)public, max-age=60, must-revalidate
Fingerprinted files (e.g. app.a1b2c3d4.js)public, max-age=31536000, immutable
Everything elsepublic, max-age=3600
Password-protected sitesprivate, no-store

Conditional requests

Every file response includes an ETag header derived from the file's SHA-256 hash. Clients can use If-None-Match to receive 304 Not Modified responses for unchanged files.

Range requests

All file responses include Accept-Ranges: bytes. Clients can use the Range header to request partial content, which is useful for streaming large media files.

Rate Limits

Rate limits are applied per endpoint category. When a limit is exceeded, the API returns 429 Too Many Requests.

ActionLimitScope
Publish (anonymous)5 per hourPer IP address
Publish (authenticated)60 per hourPer API key
Upload refresh10 per hourPer site
Worker deploy20 per hourPer API key

File and size limits

LimitAnonymousAuthenticated
Max file size250 MB5 GB
Max files per version10,000
Max total size per version10 GB

Presigned URLs expire after 1 hour. If your uploads take longer, use the refresh uploads endpoint to get new URLs.

Errors

All error responses return a JSON object with a single error field:

{
  "error": "slug is already taken"
}

Common status codes

StatusMeaning
200Success
400Bad request — invalid input, missing required fields, invalid hash format
401Unauthorized — missing or invalid Bearer token / claim token
404Not found — site or version does not exist
409Conflict — slug taken, version not pending, concurrent modification
413Payload too large — file or total site size exceeds limit
422Unprocessable — entry point file not found in uploaded worker bundle
429Too many requests — rate limit exceeded
500Internal server error
502Bad gateway — Cloudflare API failed (worker deployment failed, retry)

Common error messages

ErrorCause
"files are required"Empty file manifest
"too many files (max 10000)"Manifest exceeds file count limit
"slug is already taken"Custom slug conflicts with an existing site
"file "x" must have hash in sha256:<hex> format"Missing or malformed hash
"version is not pending"Trying to finalize or refresh an already-finalized version
"file "x": not uploaded"Finalize called before all files were uploaded
"file "x": hash mismatch"Uploaded file content does not match declared hash
"rate limit exceeded"Too many requests in the time window