# Fylo.Host API v1

Publish a static site with one request. The Fylo.Host API turns an HTML document into a live website at `your-name.fylo.dev`, served from Cloudflare's edge in 330+ cities. JSON in, JSON out, a Bearer key in the header. Built for scripts, CI and AI agents.

- Base URL: `https://fylo.host/api/v1`
- REST, JSON, Bearer auth, CORS enabled
- HTML version of this page: https://fylo.host/docs/api
- Short machine-readable summary: https://fylo.host/llms.txt

## Quickstart

Create a key in the dashboard at https://fylo.host/dashboard/settings/api, then:

```bash
# publish
curl -s https://fylo.host/api/v1/sites \
  -H "Authorization: Bearer fylo_live_..." \
  -H "Content-Type: application/json" \
  -d '{"html":"<!doctype html><h1>Hello</h1>","subdomain":"hello-world"}'

# → 201
{ "id": "NEtS3W8axVbi2aig72Jbt", "subdomain": "hello-world",
  "url": "https://hello-world.fylo.dev", "status": "active", ... }
```

The page is live at that URL before the response arrives. Anyone can open it; no account, no app.

## Authentication

Every request needs an API key in the `Authorization` header:

```
Authorization: Bearer fylo_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Keys are tied to your account and act with its plan and project limits. They are shown once when created and stored hashed; revoking one takes effect immediately. Keep keys in environment variables or a secrets manager, never in a file you publish.

A key (and an MCP connection) can publish and manage sites, but it is not a login: deleting a site or the account, billing and key management need the dashboard session.

## For AI agents (Claude Code, Codex CLI, Cursor, Cline)

Install the Fylo skill once and then just say "publish this to Fylo":

```bash
npx skills add raviblogger/fylo-host-skill --skill fylo -g
export FYLO_API_KEY=fylo_live_...
```

The skill knows how to inline assets, pick a subdomain, remember the site id in `.fylo.json` and update the same URL on the next publish. Source: https://github.com/raviblogger/fylo-host-skill

Prompt to paste into a coding agent on your machine:

```
Publish this project to Fylo.Host and give me the live URL.
If the Fylo skill is not installed, install it first: npx skills add raviblogger/fylo-host-skill --skill fylo -g
Use my FYLO_API_KEY environment variable; if it is not set, ask me for a key from https://fylo.host/dashboard/settings/api
Publish index.html as one self-contained HTML file (inline CSS and JS) to POST https://fylo.host/api/v1/sites and save the returned id in .fylo.json so future publishes update the same site.
```

Chat windows (claude.ai, ChatGPT) can't reach fylo.host from their sandboxes — connect the MCP server instead.

## Use from Claude, ChatGPT and other chat apps (MCP)

Chat apps can't run scripts against fylo.host, but they can call an MCP server. Fylo.Host is one: connect it once and say "publish this to Fylo" in the chat where you built the page. Sign-in is OAuth — no key to paste.

```
https://fylo.host/mcp
```

| Client | How to connect |
|---|---|
| Claude (claude.ai, Desktop) | Settings → Connectors → Add custom connector → name `Fylo.Host`, URL above → Connect, then Allow on the Fylo page. Pro, Max, Team and Enterprise plans. |
| ChatGPT | Settings → Connectors → Create (developer mode) → MCP server URL above, authentication OAuth. |
| Claude Code | `claude mcp add --transport http fylo https://fylo.host/mcp` then `/mcp` to sign in. |
| Cursor, Windsurf, others | Add an MCP server of type *streamable HTTP* with the URL above; they open the same sign-in page. |

Tools exposed:

- Sites: `publish_site` (html or files, optional subdomain), `update_site` (site id + html or files), `list_sites`, `list_versions`, `restore_site`
- Blog: `list_posts`, `enable_blog`, `get_post`, `create_post` (HTML body, draft or published), `update_post`, `unpublish_post`, `delete_post`
- Forms (Pro and Business): `create_form`, `form_status`, `list_leads`, `disable_form`
- Analytics: `get_analytics`
- Speed settings: `get_performance`, `set_performance`
- Custom domains (paid plans): `list_domains`, `add_domain`, `check_domain`, `remove_domain`

They call the same routes as the dashboard and the API, so plan limits and errors are identical. Connected apps are listed under Settings → API in your dashboard, where Disconnect cuts an app off immediately; tokens also expire after an hour and refresh automatically for 30 days while connected. A connected app can never delete a site, change billing or manage API keys.

**New tools not showing in Claude?** claude.ai keeps the tool list it saw when the connector was first added. Remove the connector and add it again as `https://fylo.host/mcp?v=2` (same server, fresh list), then start a new chat.

### Publish a site from chat

| You say | What happens |
|---|---|
| "Publish this to Fylo" | `publish_site` with the page's HTML → live at a generated `*.fylo.dev` URL in a few seconds. Claude keeps the site id for later changes. |
| "Publish it as *my-portfolio*" | `publish_site` with `subdomain: my-portfolio` → `https://my-portfolio.fylo.dev`. If the name is taken you're asked for another. |
| "Publish the whole project" (several pages, CSS, images) | `publish_site` with `files` → a multi-file site; relative links work as they do locally. |
| "Make the heading blue and update the site" | `update_site` → same URL, edge cache rebuilt, previous version saved. |
| "List my sites" | `list_sites` → ids and URLs. |
| "Undo that" / "put it back to yesterday's version" | `list_versions` + `restore_site` → the chosen snapshot goes live; the current one is saved first. |

### Add a contact form from chat

Fylo Forms give a static site a working contact form — submissions are spam-checked, emailed to you and listed in the dashboard. Pro and Business plans.

| You say | What happens |
|---|---|
| "Add a contact form to my *portfolio* site" | `create_form` → Claude gets the form snippet, places it in the page and calls `update_site`. The form works immediately. |
| "Any new leads this week?" | `list_leads` → recent submissions with name, email, message, country and time. |
| "Is the form still on?" / "pause the form" | `form_status` (usage vs. the monthly cap) / `disable_form`. |

The snippet is a plain HTML `<form>` posting to `https://fylo.host/api/forms/<key>` with a honeypot and a timestamp; any input names are captured, as long as the hidden `fylo_*` inputs stay.

### Publish a blog post from chat

| You say | What happens |
|---|---|
| "Enable the blog on my *portfolio* site" | `enable_blog` → the blog exists at `/blog/` with the default theme. Once per site. |
| "Write a 400-word post titled *Why static sites are fast* and publish it" | `create_post` with `status: published` → live at `/blog/why-static-sites-are-fast/`, listed on the blog index and in RSS. |
| "Draft a post about …, I'll review it first" | `create_post` as a draft → private preview link; say "publish it" later. |
| "Fix the typo in the second paragraph" | `get_post` + `update_post` → same URL, blog rebuilt. |
| "List my posts" / "take the launch post offline" | `list_posts` / `unpublish_post`. |

Posts are written as HTML by the model and sanitized on the server; the free plan allows 10 published posts, paid plans more.

### Check analytics from chat

Refer to a site by its id, its fylo subdomain or its custom domain.

| You say | What happens |
|---|---|
| "How are my sites doing this week?" | `get_analytics` without a site → views per site for the account, plus visitors in the last 5 minutes. |
| "Show analytics for *example.com* for the last 30 days" | `get_analytics` with the site and `range: 30d` → views by day, unique visitors, bounce rate, average time on page, top pages, referrers, countries and devices. |

Ranges: `today`, `7d`, `30d`, `90d`. The free plan covers `today` and `7d`. Views are sampled estimates, counted in IST days.

### Tune speed settings from chat

The same switches as the dashboard's Performance tab. Originals are never modified; every setting is reversible.

| You say | What happens |
|---|---|
| "What speed settings are on for *my-portfolio*?" | `get_performance` → every setting and its value. |
| "Turn on WebP images and lazy-load iframes" | `set_performance` with `images_webp: true, lazy_iframes: true` → saved, the site is rebuilt, and the reply reports what is actually stored. |
| "JS delay broke my menu, turn it off" | `set_performance` with `delay_js: false`. |

Settings: `fonts`, `font_mode` (selfhost, subset, system), `preload_fonts`, `minify`, `inline_css`, `delay_js`, `js_mode` (interaction, idle, defer), `js_exclude`, `selfhost_external`, `selfhost_exclude`, `images_webp`, `images_exclude`, `image_quality` (40–90), `responsive_images`, `responsive_exclude`, `size_images`, `lazy_images`, `lazy_images_exclude`, `lazy_iframes`, `youtube_previews`, `preload_lcp`, `preload_lcp_image`. On sites with many pages, change one or two per message — each change rebuilds the site.

### Connect a custom domain from chat

Starter, Pro and Business plans. The app can register the domain; the DNS records are yours to create at your DNS provider.

| You say | What happens |
|---|---|
| "Which domains do I have?" | `list_domains` → each domain, the site it points to, verified or not. |
| "Connect *example.com* to my portfolio site" | `add_domain` → the domain and its `www` form are registered, and you get two CNAME records (`@` and `www` → `fylo.host`) to add. |
| "I've added the records, check it" | `check_domain` → DNS and SSL status; the domain is marked verified once active. |
| "Remove *example.com*" | `remove_domain` → Claude asks you to confirm first. The domain and its `www` form are detached; the site stays live on its fylo subdomain. |

## Endpoints

Base URL `https://fylo.host/api/v1`. All bodies are `application/json`.

### GET /me

Returns the account behind the key. Handy to verify a key.

```json
{ "id": "dkeV...", "email": "you@example.com", "name": "You", "plan": "pro" }
```

### GET /sites

Lists up to 100 of the account's sites, newest first.

```
{ "sites": [ { "id", "subdomain", "url", "status", "created_at" }, ... ] }
```

### POST /sites

Creates a site.

| Field | Type | Notes |
|---|---|---|
| `html` | string | A complete HTML document, up to 10 MB. CSS and JS inline; images as `https://` URLs or data URIs. Required unless `files` is sent. |
| `subdomain` | string | Optional. 1–40 lowercase letters, digits, hyphens. Omit to get one generated. |
| `files` | array | Instead of `html`: a list of `{ path, content, encoding? }` objects. Text files as strings, binaries as base64 with `"encoding": "base64"`. Must include at least one .html; `index.html` becomes the entry page. Up to 2,000 files and 20 MB per request. |

```json
[ { "path": "index.html",   "content": "<!doctype html>..." },
  { "path": "css/app.css",  "content": "..." },
  { "path": "img/logo.png", "content": "iVBOR...", "encoding": "base64" } ]
```

Returns `201` with the site object plus `expires_at` (always `null` for API publishes — they belong to your account and stay up). Relative links between your files work as they would on any host. For sites over 20 MB, upload a ZIP from the homepage or dashboard.

### GET /sites/:id

Returns one site.

### PUT /sites/:id

Replaces the site's content. Same body as create: `html` for a single page (all plans), or `files` to replace the whole site (paid plans; the free plan gets `402 plan_limit` and can use `html`). The URL stays the same and the edge cache is rebuilt for you. Prefer this over creating a new site for every change — each new site uses a project slot on your plan.

```bash
curl -s -X PUT https://fylo.host/api/v1/sites/NEtS3W8axVbi2aig72Jbt \
  -H "Authorization: Bearer fylo_live_..." \
  -H "Content-Type: application/json" \
  -d '{"html":"<!doctype html><h1>Hello, v2</h1>"}'
```

## Version history

Every time a site is replaced — from the dashboard, the API or an MCP client — the previous content is saved as a whole-site version first. Free keeps 3, Starter 5, Pro 20, Business 24; older ones are pruned automatically. Versions count against your plan's storage: when a new snapshot wouldn't fit, the oldest versions are removed first, and a publish is never blocked by its own backup.

### GET /sites/:id/versions

```json
{ "site_id": "…", "limit": 20, "versions": [ { "id": "v_…", "created_at": 1758000000, "source": "mcp:Claude", "files": 12, "bytes": 348112, "pages": 3 } ] }
```

### POST /sites/:id/restore

```json
{ "version_id": "v_…" }
```

Puts that version live at the same URL and rebuilds the cache. The current content is saved as a new version first (`source: "pre-restore"`), so a restore is always reversible.

## Errors

Every error is `{ "error": { "code", "message" } }` with a matching HTTP status.

| Status | code | Meaning |
|---|---|---|
| 401 | `unauthenticated` | Key missing, malformed or revoked. |
| 400 | `missing_html` / `invalid_subdomain` | Body is missing `html`, or the slug breaks the rule. |
| 409 | `subdomain_taken` | Pick another slug or omit it. |
| 402 | `plan_limit` | Project limit for the account's plan reached. Update an existing site, delete one, or upgrade. |
| 413 | `too_large` | `html` over 10 MB. |
| 429 | `rate_limited` | Over 30 requests per minute on this key. Retry after 60 s. |
| 400 | `invalid_path` / `duplicate_path` / `invalid_content` | A `files` entry has a bad path (no `..`, no leading slash), repeats a path, or has non-string / bad base64 content. |
| 422 | `rejected` | A file was blocked by content moderation (executables, pirated-media names). |
| 502 | `partial_publish` | The site was created with its entry page but attaching the other files failed; the response includes `site`. Retry with `PUT /sites/:id`. |
| 404 | `not_found` | Unknown endpoint or a site that isn't yours. |

## Limits

- **API:** 30 requests per minute per key.
- **AI apps (MCP):** 30 tool calls per minute per account, and 60 per hour for the tools that rebuild a site: `publish_site`, `update_site`, `restore_site`, `set_performance`.
- **New sites:** 20 per hour on paid plans, 5 on the free plan, per account, from the API and from AI apps alike. Updating an existing site does not count.
- Over a limit the API answers `429 rate_limited`; an MCP tool returns an error the app can read and retry later. Limits are per account, never per IP.
- Up to 5 active keys per account.
- Site count and storage follow your plan — see https://fylo.host/#pricing. Free accounts get 1 site and 10 MB.
- The API is served only on `fylo.host`; it is never exposed on hosted sites' domains.

## Managing keys

Create, list and revoke keys under Settings → API in your dashboard (https://fylo.host/dashboard/settings/api). The same actions are available to a logged-in browser session at `GET/POST /api/keys` and `DELETE /api/keys/:id`; these endpoints use your session cookie, not a Bearer key.
