# beacon.host full reference

> Web hosting for AI agents: HTML reports, dashboards, mini apps and static sites at https://<slug>.beacon.host and Cloudflare Workers at https://<slug>.worker.beacon.host. This file is the complete reference. The short version is https://beacon.host/skill.md. OpenAPI: https://beacon.host/openapi.json

Publish a file or folder, get back `https://<slug>.beacon.host`. Base URL: `https://beacon.host`. The first part below is the same as skill.md; the endpoint reference follows.

## When to use

The user wants something online with a link: an HTML page, a static build (`dist/`, `build/`, `out/`, `public/`), a report, dashboard, slide deck, game, PDF or image. Or a temporary preview that expires on its own, or a page behind a password. Or they need a backend a static page can call: an API, a form handler, a GitHub or Stripe webhook receiver, a Hono or Workers app.

If you can't make network requests from where you run (a sandbox without internet), don't guess: tell the user to open https://beacon.host/html-to-url and drop or paste the file there.

## Requirements

`curl` and network access to `beacon.host`. The three-call API for big sites also needs `*.r2.cloudflarestorage.com` (file bytes go straight to storage through presigned URLs). Python 3 or the `beacon` CLI are optional shortcuts.

## Publish a file: one call, no account

```bash
curl -sS https://beacon.host/v1/publish -H 'X-Beacon-Client: claude-code/2.0' -F file=@index.html
```

The response is JSON: `url`, `claimUrl`, `claimToken`, `expiresAt`, and `fileUrl` (the direct link when you sent one file). A single `.html` file becomes the home page whatever it's called. More files: repeat `-F file=@path` (use `-F "file=@app.js;filename=js/app.js"` to keep a folder path), or zip the folder and send `-F archive=@site.zip`. Add `-F slug=my-site` to pick the subdomain, `-H 'Accept: text/plain'` to get only the link. Up to 4 MB of files per call without an API key, 10 MB with one; bigger sites use the three calls below.

Private link? Add `-F username=client -F password=...` and the site is behind a browser login prompt before it goes live, also without an account. Give the user the URL, username and password, and send the password separately from the link.

Shorter life? Add `-F expires=2h` (`90m`, `2h`, `7d` or RFC 3339). Without an account a site can only expire sooner than 24 hours.

## Big files and folders: three calls

Create the site with a file list, upload each file, go live. Files up to 250 MB each without an account.

```bash
F=index.html                                        # file to publish
CLIENT='X-Beacon-Client: claude-code/2.0'           # your harness/version, see below
HASH=$( (sha256sum "$F" 2>/dev/null || shasum -a 256 "$F") | cut -d' ' -f1)
SIZE=$(wc -c < "$F" | tr -d ' ')
curl -s https://beacon.host/v1/sites -H "$CLIENT" -H 'Content-Type: application/json' \
  -d "{\"files\":[{\"path\":\"index.html\",\"size\":$SIZE,\"contentType\":\"text/html; charset=utf-8\",\"hash\":\"sha256:$HASH\"}]}" > site.json
curl -s -X PUT "$(jq -r '.uploads.pending[0].uploadUrl' site.json)" \
  -H 'Content-Type: text/html; charset=utf-8' --data-binary @"$F"
curl -s -X POST "https://beacon.host$(jq -r .version.finalizeUrl site.json)" \
  -H "$CLIENT" -H "X-Claim-Token: $(jq -r .claimToken site.json)"
jq -r '.site.url, .claimUrl' site.json
```

No `jq`? Run the calls one at a time and copy the values out of the JSON yourself.

Rules:
- `hash` is `sha256:` plus 64 lowercase hex characters of the file bytes. `size` is in bytes.
- PUT each file in `uploads.pending` to its `uploadUrl` with exactly the `Content-Type` you declared. Files the server already has come back in `uploads.skipped`; don't upload those.
- Finalize an anonymous site with `X-Claim-Token: <claimToken>`. Without it you get `401`.
- More files: list them all in `files` (nested paths like `js/app.js` are fine) and PUT each one. `/` serves `index.html`.

## Publish a folder

```bash
curl -fsSLO https://beacon.host/publish.py      # download once
python3 publish.py ./dist                       # folder or single file
python3 publish.py ./dist my-slug               # pick the subdomain
```

Standard-library Python. Prints JSON with `url`, `slug`, `expiresAt`, `claimUrl` and, for one file, `fileUrl`, and saves the slug and claim token in `.beacon/` so running it again updates the same site. `--expires 2h` makes the link expire sooner. Set `BEACON_CLIENT=<harness>/<version>` to identify yourself.

Or the CLI (sites and workers): `curl -fsSL https://beacon.host/install.sh -o install-beacon.sh && sh install-beacon.sh`, then `beacon deploy --pretty`. Windows: `iwr https://beacon.host/install.ps1 -useb | iex`.

## Share a single file (PDF, image, HTML)

```bash
curl -sS https://beacon.host/v1/upload -F file=@report.pdf     # prints https://<slug>.beacon.host/report.pdf
```

Files keep their content type, so HTML renders and PDFs and images open in the browser instead of downloading. Give the user the direct file link (`fileUrl`); chat apps such as Slack preview images from it. The site root shows a small viewer page. `/v1/upload` prints just the link and puts the claim details in the `X-Claim-Token`, `X-Claim-Url` and `X-Expires-At` response headers; `/v1/publish` returns them as JSON.

## What to tell the user

1. Put the site URL on a line by itself, with nothing else on that line.
2. If the site is anonymous, say it expires in 24 hours, and give them the `claimUrl` so they can keep it. Copy it exactly as returned, character for character. Don't shorten, wrap or reformat it; the code after `#` is 47 characters and the link breaks if any are lost.
3. Never show an API key in the chat. The `claimUrl` already carries the claim code, so don't paste the `claimToken` separately.
4. If they ask how keeping the site works, link https://beacon.host/guides/host-static-site-no-signup#claim-the-site-later

```
Your site is live:

https://quiet-river-42.beacon.host

It will be deleted in 24 hours. To keep it, open this link and sign in with your email:
<claimUrl, exactly as returned>
```

Keep the `claimToken` (`ctk_...`) with the project. It is the only way to update, delete or claim an anonymous site.

## Sign up or sign in (email code)

There's no password. Signing up and signing in are the same two calls:

```bash
curl -s https://beacon.host/v1/auth/send-otp -H "$CLIENT" -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com"}'
```

Ask the user: "I sent a 6-digit code to user@example.com. What is it?" Then:

```bash
curl -s https://beacon.host/v1/auth/verify-otp -H "$CLIENT" -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","code":"123456"}'
# -> {"apiKey":"chk_...","account":{"id":"...","email":"..."}}
```

Codes expire after 10 minutes, and 5 wrong tries cancel a code.

## Save the API key yourself

Write it to `~/.config/beacon/config.json` as `{"apiKey":"chk_..."}` and `chmod 600` the file. `publish.py` and the `beacon` CLI read it from there (or from the `BEACON_API_KEY` environment variable). Do this yourself; don't ask the user to, and don't show the key in the chat.

Send it as `Authorization: Bearer chk_...`. With a key, sites last until you delete them or until an `expiresAt` you set, workers are available, `GET /v1/sites` lists sites, and `GET /v1/account` shows the account and its usage.

Move an anonymous site into the account:

```
POST /v1/sites/<slug>/claim
Authorization: Bearer chk_...
{"claimToken": "ctk_..."}
```

## Identify your client

Send `X-Beacon-Client: <harness>/<version>` on every request, for example `claude-code/2.0`, `cursor/1.7`, `codex/0.40`, `hermes/1.2` or `my-script/1`. It's optional. It tells us which agents use beacon.host so we can fix what breaks for them.

## Update a site

```
POST /v1/sites/<slug>/versions          {"files": [...same manifest format...]}
X-Claim-Token: ctk_...   (anonymous site)   or   Authorization: Bearer chk_...   (owned site)
```

Upload the `pending` files, then POST the returned `finalizeUrl` with the same header. `POST /v1/sites` with a slug that exists returns `409`; use the versions endpoint instead. `GET /v1/sites/<slug>` (same headers) shows the site and its live version. Shortcut: `curl -sS "https://beacon.host/v1/publish?slug=<slug>" -H 'X-Claim-Token: ctk_...' -F file=@index.html` sends a new version in one call.

## Temporary links

Anonymous sites last 24 hours from the first publish and updates don't reset the clock; they can be made to expire sooner (`expires` on `/v1/publish`, `--expires` on publish.py, or `PATCH /v1/sites/<slug>/metadata` `{"expiresAt":"<RFC 3339>"}`), to any time up to 24 hours after the first publish, never later. With an API key any future time works, or `null` for none. At expiry the URL returns 404 and the site is deleted. More: https://beacon.host/guides/temporary-website-hosting

## Workers (API backends)

Serverless JavaScript/TypeScript (Hono works) on Cloudflare at `https://<slug>.worker.beacon.host`. Needs a beacon.host API key, not a Cloudflare account, and the URL stays up until you delete it. Sign-in works without a browser: POST `/v1/auth/send-otp` with the user's email, ask the user for the 6-digit code, then POST `/v1/auth/verify-otp`. Hono: `beacon init my-api --template hono && cd my-api && npm install && beacon deploy --pretty`. Replace `chk_...` with the key:

```bash
curl -s -X POST https://beacon.host/v1/workers -H "Authorization: Bearer chk_..." \
  -H 'Content-Type: application/json' -d '{"slug":"my-api"}'
curl -s -X POST https://beacon.host/v1/workers/my-api/deploy -H "Authorization: Bearer chk_..." \
  -F 'metadata={"entryPoint":"index.js","compatibilityDate":"2024-09-23","compatibilityFlags":["nodejs_compat"]}' \
  -F 'file=@index.js'
```

Upload a JavaScript ES module (`export default { fetch }`); compile TypeScript to JavaScript first. `beacon deploy` bundles npm imports with esbuild. Secrets: `PUT /v1/workers/<slug>/secrets/<NAME>` with `{"value":"..."}`; set them after the first deploy and they stay set across redeploys and rollbacks. Scheduled (cron) runs are not available yet. Workers have no built-in database or KV; keep state in an outside service and put its key in a secret. Form submissions are received and forwarded (email, a spreadsheet, your database), not stored. A site and a worker can't share a slug, so pair them with two: `my-app.beacon.host` (frontend) calls `my-app-api.worker.beacon.host` (API).

Webhook receiver: read the raw body with `await request.text()`, verify the sender's HMAC signature against a Worker secret, return 2xx within 10 s (GitHub does not retry), forward elsewhere for storage. Walkthrough with GitHub and Stripe code: https://beacon.host/guides/deploy-webhook-from-agent. Workers page: https://beacon.host/workers. Full worker reference: https://beacon.host/llms-full.txt

## Other site operations

| Do this | Call |
|---|---|
| Password-protect (anonymous sites too) | `PUT /v1/sites/<slug>/password` `{"username":"u","password":"p"}` (max 72 chars) |
| Remove password | `DELETE /v1/sites/<slug>/password` |
| Title / description / OG image / expiry | `PATCH /v1/sites/<slug>/metadata` `{"title":"...","description":"...","ogImagePath":"og.png","expiresAt":"..."}` |
| List versions | `GET /v1/sites/<slug>/versions` |
| Roll back | `POST /v1/sites/<slug>/versions/<versionId>/rollback` |
| Delete | `DELETE /v1/sites/<slug>` |
| Expired upload URLs (after 1 hour) | `POST /v1/sites/<slug>/versions/<versionId>/uploads/refresh` `{"paths":["index.html"]}` |

All of these take `X-Claim-Token` (anonymous) or `Authorization: Bearer` (owned).

## Errors

| Status | Meaning | Fix |
|---|---|---|
| 401 on finalize or update | Missing credentials | Send `X-Claim-Token` from the create response, or your Bearer key for owned sites |
| 404 on a site | Not your site, wrong key, or expired | Check `GET /v1/sites` |
| 405 | Wrong method | The `Allow` header lists the right one |
| 409 on create | Slug taken | Use another slug, or `POST /v1/sites/<slug>/versions` if it's yours |
| 400 on finalize | `file "x": not uploaded`, or size/hash mismatch | PUT the named file to its `uploadUrl` (the exact bytes you hashed), then finalize again |
| 409 on finalize | `version is not pending` | Already live; create a new version to change it |
| 429 | Rate limit | Anonymous: 5 publishes an hour per IP (new sites and updates). Sign in for 60 an hour |

Errors are JSON: `{"error":"..."}`, often with a `hint` and a `docs` link.

## Limits

Anonymous: sites last 24 hours, 250 MB per file, 5 publishes an hour. Signed in: permanent sites, 5 GB per file, 60 publishes an hour. Both: 10 GB per site and 10,000 files per version. Workers: 3 MB compressed bundle, 100 files, 20 deploys an hour. `GET /v1/limits` returns the current numbers.

If these docs and the live API disagree, trust the API: its error messages say what to fix.

## MCP server

Clients that speak MCP can publish without curl. The remote server is `https://beacon.host/mcp` (Streamable HTTP, no OAuth) with three tools: `publish_site` (send file contents, get the URL and claim link back), `get_site` and `get_docs`. Anonymous by default; send `Authorization: Bearer chk_...` to publish into an account.

```bash
claude mcp add --transport http beacon https://beacon.host/mcp
```

Setup for Codex, Cursor, VS Code, claude.ai and ChatGPT: https://beacon.host/mcp. `publish_site` takes up to 4 MB of file content per call without an API key and 10 MB with one, and optional `password` and `expires_at`; use the HTTP API above for anything bigger.

Guides: host a static site with no signup (https://beacon.host/guides/host-static-site-no-signup), share an HTML file as a link (https://beacon.host/guides/share-html-file-as-link), temporary links (https://beacon.host/guides/temporary-website-hosting), password-protect an HTML page (https://beacon.host/guides/password-protect-html-page), deploy a webhook receiver (https://beacon.host/guides/deploy-webhook-from-agent).

# Endpoint reference

## Static Sites API

### Publish in one request {#one-request-publish}

```
POST /v1/publish
Authorization: Bearer <key>     (optional; omit for anonymous)
X-Claim-Token: ctk_...          (optional; with ?slug=, updates that anonymous site)
```

The body is the files themselves, in any of these forms:

- `multipart/form-data`: one or more `file` fields (the part's filename is the path; a lone `.html` file becomes `index.html`), or an `archive` field with a `.zip` or `.tar.gz` (a single top-level folder is stripped). Optional fields: `slug`, `title`, `expires` (`90m`, `2h`, `7d` or RFC 3339), `username` and `password` (HTTP Basic Auth on the site).
- `application/json`: `{"files":[{"path":"index.html","content":"...","encoding":"utf8|base64"}],"slug":"...","title":"...","expires_at":"2h","password":{"username":"u","password":"p"}}` (the MCP `publish_site` shape).
- `application/zip`, `application/x-tar` or `application/gzip`: an archive, unpacked as above.
- Anything else: one file. Name it with `?name=report.pdf`; without a name, HTML becomes `index.html` and other types get a generic name with the right extension.

Query parameters: `slug`, `title`, `expires`. Limits: 4 MB of files per request without an API key, 10 MB with one, 1,000 files; bigger sites get `413` and should use the three-call flow below. Same rate limits as `POST /v1/sites` (5 an hour per IP without a key).

Response `201` (new site) or `200` (update):

```json
{
  "url": "https://bright-river-42.beacon.host",
  "pageUrl": "https://bright-river-42.beacon.host/",
  "fileUrl": "https://bright-river-42.beacon.host/report.pdf",
  "slug": "bright-river-42",
  "updated": false,
  "anonymous": true,
  "expiresAt": "2026-10-02T09:14:03Z",
  "claimUrl": "https://beacon.host/claim/bright-river-42#ctk_...",
  "claimToken": "ctk_...",
  "passwordProtected": false,
  "versionId": "...",
  "fileCount": 1,
  "totalBytes": 18342
}
```

`fileUrl` is set when one file was published; `fileUrls` lists up to 20 files otherwise. Send `Accept: text/plain` to get only the link (`fileUrl`, else `pageUrl`) as the body, with `X-Claim-Token`, `X-Claim-Url` and `X-Expires-At` response headers.

`POST /v1/upload` is the same endpoint with plain-text output by default (send `Accept: application/json` for JSON), and `PUT /v1/upload/<name>` takes the raw body (`curl -T report.pdf https://beacon.host/v1/upload/report.pdf`).

### Create a site

```
POST /v1/sites
Authorization: Bearer <key>  (optional — omit for anonymous)
Idempotency-Key: <unique string>  (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.beacon.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://beacon.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 <uploadUrl>
Content-Type: text/html

<file contents>
```

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 <key>
```

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 <key>
Content-Type: application/json

{"files": [...]}
```

Same flow: upload files, then finalize.

### List sites

```
GET /v1/sites?limit=20&offset=0
Authorization: Bearer <key>
```

Pagination: `limit` (1-100, default 20), `offset` (default 0).

### Update metadata

```
PATCH /v1/sites/:slug/metadata
Authorization: Bearer <key>     (or X-Claim-Token: ctk_... for an anonymous site)
Content-Type: application/json

{
  "title": "My Site",
  "description": "A demo",
  "ogImagePath": "preview.png",
  "expiresAt": null
}
```

`expiresAt` is an RFC 3339 time, or `null` for no expiry (account-owned sites only). Anonymous sites may only shorten their expiry, to at most 24 hours after creation; a later time gets `400`. Claim the site to keep it longer.

### Set password protection

```
PUT /v1/sites/:slug/password
Authorization: Bearer <key>
Content-Type: application/json

{"username": "demo", "password": "s3cret"}
```

Anonymous sites can be locked before anyone claims them, with the claim token:

```
PUT /v1/sites/:slug/password
X-Claim-Token: ctk_...
Content-Type: application/json

{"username": "demo", "password": "s3cret"}
```

Username 1-128 characters; password max length: 72 characters (bcrypt limit). Visitors get the browser's Basic Auth prompt; responses are `Cache-Control: private, no-store`. Guide: https://beacon.host/guides/password-protect-html-page

### Remove password protection

```
DELETE /v1/sites/:slug/password
Authorization: Bearer <key>
```

### Delete a site

```
DELETE /v1/sites/:slug
Authorization: Bearer <key>     (or X-Claim-Token: ctk_... for anonymous sites)
```

Returns `200` with `{"success": true}`.

### Claim an anonymous site

Requires both Bearer auth (to identify the account) and the claim token:

```
POST /v1/sites/:slug/claim
Authorization: Bearer <key>
Content-Type: application/json

{"claimToken": "ctk_..."}
```

### List versions

```
GET /v1/sites/:slug/versions?limit=20&offset=0
Authorization: Bearer <key>
```

### Rollback to a version

```
POST /v1/sites/:slug/versions/:versionId/rollback
Authorization: Bearer <key>
```

### Refresh upload URLs

If presigned URLs have expired (after 1 hour):

```
POST /v1/sites/:slug/versions/:versionId/uploads/refresh
Authorization: Bearer <key>
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 <key>
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 <key>`.

### Create a worker

```
POST /v1/workers
Authorization: Bearer <key>
Idempotency-Key: <unique string>  (optional — prevents duplicate creates on retry)
Content-Type: application/json

{"slug": "my-api"}
```

Response:

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

### List workers

```
GET /v1/workers
Authorization: Bearer <key>
```

### Get worker details

```
GET /v1/workers/:slug
Authorization: Bearer <key>
```

Response:

```json
{
  "slug": "my-api",
  "url": "https://my-api.worker.beacon.host",
  "currentDeployment": {
    "id": "01JQDEP...",
    "version": 3,
    "entryPoint": "index.js",
    "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 <key>
Content-Type: multipart/form-data

Form fields:
  metadata = {"entryPoint": "index.js", "compatibilityDate": "2024-09-23", "compatibilityFlags": ["nodejs_compat"]}
  file = @index.js
  file = @utils.js
```

Files are deployed as-is: Beacon doesn't compile TypeScript or bundle npm imports, so upload JavaScript ES modules (`export default { fetch }`). curl sends only a file's base name, so keep extra modules next to `index.js` and import them as `./utils.js`. Compile TypeScript first (for example `npx esbuild index.ts --format=esm --outfile=index.js`). `beacon deploy` runs your package.json `build` script, or bundles npm imports with esbuild, before it uploads.

curl example:

```bash
curl -X POST https://beacon.host/v1/workers/my-api/deploy \
  -H "Authorization: Bearer your-api-key" \
  -F 'metadata={"entryPoint":"index.js","compatibilityDate":"2024-09-23","compatibilityFlags":["nodejs_compat"]}' \
  -F "file=@index.js"
```

Metadata fields:
- `entryPoint` (required) — main module filename
- `compatibilityDate` (required) — Cloudflare compatibility date
- `compatibilityFlags` — e.g. `["nodejs_compat"]`
- `crons` — cron expressions, e.g. `["0 * * * *"]`. Accepted and stored, but scheduled runs are not available yet; don't rely on them.

Response:

```json
{
  "slug": "my-api",
  "url": "https://my-api.worker.beacon.host",
  "deploymentId": "01JQDEP...",
  "version": 3,
  "sizeBytes": 1234,
  "fileCount": 2,
  "entryPoint": "index.js",
  "deployedAt": "2026-03-20T15:10:00Z"
}
```

Deploy is idempotent — if the script hash and metadata match the current deployment, the existing deployment is returned.

### Delete a worker

```
DELETE /v1/workers/:slug
Authorization: Bearer <key>
```

Returns `204 No Content`.

### Set a secret

```
PUT /v1/workers/:slug/secrets/:name
Authorization: Bearer <key>
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 <key>
```

Returns `204 No Content`.

### List secret names

```
GET /v1/workers/:slug/secrets
Authorization: Bearer <key>
```

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 <key>
```

### Rollback to previous deployment

```
POST /v1/workers/:slug/rollback
Authorization: Bearer <key>
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.beacon.host",
  "deploymentId": "01JQNEW...",
  "version": 4,
  "sizeBytes": 1234,
  "fileCount": 2,
  "entryPoint": "index.js",
  "deployedAt": "2026-03-20T16:00:00Z",
  "rolledBackFrom": "01JQDEP..."
}
```

### Real-time logs

```
GET /v1/workers/:slug/logs/tail
Authorization: Bearer <key>
```

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://beacon.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

Hono is an npm package, so deploy this with `beacon deploy`, which bundles it with esbuild. The raw deploy endpoint takes the files as-is and can't resolve `import ... from 'hono'`.

```typescript
import { Hono } from 'hono'

const app = new Hono()

app.get('/api/hello', (c) => {
  return c.json({ message: 'Hello from beacon.host!' })
})

app.post('/api/contact', async (c) => {
  const body = await c.req.json()
  return c.json({ ok: true })
})

export default app
```

Deploy: `my-app.beacon.host` serves the frontend, `my-app-api.worker.beacon.host` serves the API (a site and a worker can't share a slug).

---

## 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 |
|--------|------|------|-------------|
| GET | `/v1` | None | Entry points and docs links |
| POST | `/v1/auth/send-otp` | None | Send OTP code to email (aliases: `/v1/auth/login`, `/v1/auth/signup`, `/v1/signup`, `/v1/login`) |
| POST | `/v1/auth/verify-otp` | None | Verify OTP, get API key (aliases: `/v1/auth/verify`, `/v1/auth/otp/verify`) |
| GET | `/v1/account` | Bearer | Your account and usage (alias: `/v1/me`) |
| GET | `/v1/limits` | None | Rate limits and size caps as JSON |
| GET | `/v1/openapi.json` | None | OpenAPI 3.1 spec (same as `/openapi.json`) |
| POST | `/v1/publish` | Optional | Publish files in one request (multipart, raw body, zip or JSON) |
| POST, PUT | `/v1/upload`, `/v1/upload/:name` | Optional | Same as `/v1/publish`, prints just the link |
| POST | `/v1/sites` | Optional | Create site with file manifest |
| GET | `/v1/sites` | Bearer | List your sites |
| GET | `/v1/usage` | Bearer | Account usage: site/worker counts, bytes, recent activity |
| GET | `/v1/sites/:slug` | Bearer or Claim | Site details and live version (404 without credentials) |
| PATCH | `/v1/sites/:slug/metadata` | Bearer or Claim | Update title, description, OG image, expiry (anonymous: shorten only) |
| 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 (200 `{"success":true}`) |
| 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 |
