htmlhost.co

Documentation

Everything you need to deploy, manage, and edit HTML sites from the browser, terminal, or API.

Quick start

Get a live site in under 30 seconds. Sign in with Google, GitHub or an email link, then publish. Upgrade to Pro for more sites, longer TTLs, and custom domains.

Option 1: Browser

Go to htmlhost.co, paste or drop an HTML file, pick a TTL, and hit Publish. Done.

Option 2: CLI

bash# Install globally
npm i -g htmlhost-cli

# Authenticate — opens your browser to htmlhost.co/settings#keys
htmlhost login

# Deploy current directory (shows file list, asks for confirmation)
htmlhost deploy
# ✓ Live at https://bold-fern-x3k.htmlhost.co
#   Linked → .htmlhost (future deploys update this site)

Option 3: cURL

bashcurl -X POST https://htmlhost.co/api/sites \
  -H "Authorization: Bearer hh_live_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Hello World</h1>","ttl":"7d"}'

CLI reference

The htmlhost CLI is a zero-dependency Node.js tool. Requires Node 18+.

bash# Install
npm i -g htmlhost-cli

# Update to the latest version
npm update -g htmlhost-cli

# Check your installed version
htmlhost --version

htmlhost login

Authenticate with an API token. Running this command automatically opens Settings → API keys in your browser. Paste the token back in the terminal when prompted. Credentials are saved to ~/.htmlhostrc.

htmlhost deploy [file|dir]

Deploy an HTML file or directory and get a live URL. Passing a directory (or no argument) deploys the entire folder as a multi-page site — HTML pages and all other assets (images, CSS, JS, fonts) are packaged and uploaded together. Passing a single file deploys just that file with local assets auto-inlined.

Parameter
Type
Description
--ttl <value>stringSet TTL: 1d, 7d, 30d, never
--slug <slug>stringRe-deploy to a specific site
--title <title>stringSet the site title
--newflagForce a new site (ignore .htmlhost link)
--no-assetsflagSkip asset inlining for single-file deploys
--no-pull-checkflagSkip remote change detection before re-deploy
--jsonflagOutput result as JSON (for CI/CD)

Multi-page deploy

Deploy a directory as a complete multi-page website — HTML pages and all other files (images, CSS, JS, fonts) are uploaded together. Point htmlhost deploy at a folder (or run it with no arguments from your project root). The CLI shows you a file tree and asks for confirmation before uploading. Each HTML file maps to a clean URL:

bash# Deploy current directory
htmlhost deploy

# Deploy a specific directory
htmlhost deploy ./my-project

# File mapping:
#   index.html       → /
#   about.html       → /about
#   contact.html     → /contact
#   blog/index.html  → /blog
#   blog/post-1.html → /blog/post-1

# ✓ Live at https://bold-fern-x3k.htmlhost.co
#   3 pages · 5 assets · 30d TTL

Add a 404.html file to your project and it will be served automatically when visitors access a non-existent page. Page limits: Free plan supports 10 pages, Pro supports 100.

Automatic asset processing

When you deploy, the CLI scans your HTML for local file references and makes the deployed site fully self-contained. CSS and JS files are inlined directly into the HTML, while binary assets (images, fonts, SVGs, PDFs) are uploaded to your media library with URLs rewritten automatically.

If a CSS file contains url() references (e.g. background images, font files), those assets are uploaded first and the URLs inside the CSS are rewritten before the stylesheet is inlined.

CSS/JS files ≥ 512 KB are uploaded to the media library instead of inlined, to avoid bloating the HTML. Files larger than 10 MB are skipped entirely. External URLs (https://, data:, etc.) are left untouched.

bash# Deploy with automatic asset processing (default)
htmlhost deploy
# ✓ Found 4 local assets to process
#
# ✓ styles.css (4.2 KB) → inlined as <style>
#   ✓ hero.png (340 KB) → https://htmlhost.co/m/c9d0e1f2
#   ✓ Inter.woff2 (92 KB) → https://htmlhost.co/m/d3e4f5a6
# ✓ app.js (12.1 KB) → inlined as <script>
# ✓ logo.svg (2.8 KB) → https://htmlhost.co/m/a1b2c3d4
#
# ✓ Inlined 2 files into HTML
# ✓ Uploaded 3 assets to media library

# Skip asset processing
htmlhost deploy --no-assets

How each file type is handled:

  • <link href> — CSS files are inlined as <style> blocks (with nested url() assets uploaded first)
  • <script src> — JS files are inlined as <script> blocks
  • <img src>, <img srcset> — images uploaded to media library
  • <video src>, <video poster>, <audio src> — media uploaded
  • url() in <style> blocks, inline styles, and external CSS files — assets uploaded, URLs rewritten
  • Fonts (WOFF, WOFF2, TTF, OTF), SVGs, PDFs, ICO — uploaded to media library

Project linking (.htmlhost)

After the first deploy, the CLI saves the site slug to a .htmlhostfile in your project directory. On subsequent deploys, the CLI automatically re-deploys to the same site — no prompt needed for directory deploys. For single-file deploys, you'll be asked:

text? This project is linked to bold-fern-x3k.htmlhost.co

    1. Overwrite existing site
    2. Create a new site instead
    3. Cancel
    4. Always overwrite (remember for this project)
    5. Always create new (remember for this project)

  Enter choice (1-5):

Choosing option 4 or 5 saves the preference to .htmlhost, so you won't be asked again. Use --new to skip the prompt and force a new site.

bash# First deploy — creates site and .htmlhost link
htmlhost deploy

# Second deploy — re-deploys to the same site (directory: Y/n confirm; file: menu)
htmlhost deploy

# Deploy a specific file
htmlhost deploy contact.html

# Force a new site (bypass .htmlhost)
htmlhost deploy --new

htmlhost agents [dir]

Building with an AI coding tool (Claude Code, Codex, Cursor, Copilot…)? Run this once in your project. It writes htmlhost's instructions (hosting rules, the CLI, and how to build forms that deliver) into AGENTS.md and adds an @AGENTS.md import to CLAUDE.md, so your AI builds features that work on htmlhost. Only a marked block is managed, so your own notes in those files are kept. The instructions are added automatically by htmlhost cloneand kept up to date on each deploy. Agent files are never deployed.

bashhtmlhost agents              # add or refresh the instructions
htmlhost agents --print      # print them instead
htmlhost agents --no-claude  # leave CLAUDE.md alone

Tools that can browse can also read the guide directly at htmlhost.co/agents.md.

htmlhost list

List all your sites in a formatted table showing slug, URL, size, TTL, and expiry.

htmlhost delete <slug>

Delete a site by slug. Use --force to skip the confirmation prompt.

htmlhost open <slug>

Open a deployed site in your default browser.

bashhtmlhost open bold-fern-x3k

htmlhost whoami

Show the authenticated user's email, handle, and plan.

htmlhost logout

Remove saved credentials from ~/.htmlhostrc.

htmlhost recipients [slug]

Manage form-notification recipients for a site. Without a subcommand, lists all recipients and their status. If no slug is given, the site is detected from the .htmlhost file in the current directory.

bashhtmlhost recipients my-site                      # list recipients
htmlhost recipients add my-site user@example.com  # invite a recipient
htmlhost recipients remove my-site user@example.com  # remove (prompts for confirmation)
htmlhost recipients resend my-site user@example.com  # resend a pending invitation

Use --force to skip the confirmation prompt on remove, and --json for machine-readable output.

htmlhost versions [slug]

List a site's published versions, newest first, with who published each one and what it changed. Every deploy and every save in the web editor is a version. Without a slug, the linked site is used.

htmlhost rollback [slug] [version]

Make an earlier version live again. Without a version number, it goes back to the version before the live one. The old version is published again as a new version, so the one you replaced stays in history and you can switch back. Restoring needs the site owner to be on Pro; collaborators who can publish can restore too. Run htmlhost pull afterwards so your local files match.

bashhtmlhost versions my-site       # list versions
htmlhost rollback               # linked site: back one version
htmlhost rollback my-site 12    # make v12 live again
htmlhost rollback my-site 12 -f # skip the confirmation

Sync & pull

The CLI supports bidirectional sync with htmlhost.co. When you or a collaborator edits a site through the web editor, you can pull those changes back to your local project. The CLI also detects remote changes automatically before every re-deploy.

htmlhost status [dir]

Like git status for a linked folder: shows which site it deploys to, whether the live site changed since your last deploy or pull, and what the next htmlhost deploy would add, modify, or remove. It never writes anything.

Parameter
Type
Description
--jsonflagOutput the status as JSON
bashhtmlhost status

# Example output:
# ● Linked to my-site.htmlhost.co (folder)
# ✓ Live site unchanged since your last sync (v5)
#
#   Changes to deploy:
#    + new:      contact.html
#    ~ modified: index.html
#    - removed:  old.html (only on the live site; deploy deletes it)

htmlhost pull [slug]

Download remote changes from an htmlhost.co site to your local project. If the project is linked via .htmlhost, the slug is detected automatically. Otherwise, pass it as an argument.

Parameter
Type
Description
--force, -fflagPull without confirmation prompt
--dry-runflagShow what would change without writing files
bash# Pull changes for the linked site
htmlhost pull

# Pull a specific site by slug
htmlhost pull my-site

# Show what would change without writing
htmlhost pull --dry-run

# Example output:
# ● Pulling from my-site.htmlhost.co…
#   Version 5 · Updated 2h ago
#
#    + New:       about.html (1.2 KB)
#    ~ Modified: index.html
#    ~ Modified: css/style.css
#
# ● 1 new, 2 modified files to pull
# ? Pull these changes? (y/N)

Pulled files overwrite local versions. Files that exist locally but not on the remote are left untouched — the CLI never deletes local files during a pull.

htmlhost clone <slug> [directory]

Clone a deployed site into a new local directory. This is useful for sites that were created through the web editor and haven't been deployed from the CLI before. After cloning, use htmlhost deploy to push changes and htmlhost pull to sync remote edits.

bash# Clone into a new directory named after the slug
htmlhost clone my-site
# → creates ./my-site/ with all files + .htmlhost link

# Clone into a specific directory
htmlhost clone my-site ./my-project

# After cloning:
cd my-site
htmlhost deploy   # push changes
htmlhost pull     # sync remote edits

Use --force to overwrite an existing non-empty directory.

Pre-deploy sync check

When re-deploying to a linked site, the CLI automatically checks if the remote site was modified since your last deploy or pull. If remote changes are detected, you'll be prompted before overwriting:

text! Remote changes detected — my-site.htmlhost.co was modified since your last deploy.
    Remote version: 5 · Local version: 3

? What would you like to do?

    1. Pull changes first, then deploy
    2. Deploy anyway (overwrites remote changes)
    3. Cancel

  Enter choice (1-3):

Use --no-pull-check to skip this check (useful for CI/CD pipelines where you always want to deploy).

MCP server

Connect Claude, Cursor, VS Code, Windsurf, Codex or any other MCP client to your account, and your AI can deploy, list, pull and delete your sites for you. It uses the same token as htmlhost login.

bash# Claude Code
claude mcp add htmlhost -- npx -y htmlhost-mcp

Setup for every client, the full tool reference and troubleshooting are in the MCP server guide.

REST API

All endpoints accept Authorization: Bearer <token> headers. Generate tokens at Settings → API keys.

Base URL: https://htmlhost.co

GET
/api/sites
List all your sites with usage stats
POST
/api/sites
Deploy a new site or re-deploy to existing slug
DELETE
/api/sites?slug={slug}
Delete a site by slug
GET
/api/sites/pull?slug={slug}
Pull site content (pages + assets) for CLI sync
GET
/api/sites/{slug}/versions
List a site's published versions, newest first
POST
/api/sites/{slug}/versions/{n}/restore
Make version n live again (Pro)
GET
/api/media
List your media files (supports ?search=, ?type=, ?limit=, ?offset=)
POST
/api/media
Upload a media file (multipart/form-data)
DELETE
/api/media
Delete a media file by ID
GET
/api/tokens
List your API tokens
POST
/api/tokens
Create a new API token
DELETE
/api/tokens?id={id}
Revoke an API token
GET
/api/tokens/me
Validate token and return user info

Deploy

POST /api/sites

Create a new site or update an existing one. Supports both single-page and multi-page deploys.

Single-page deploy

Parameter
Type
Description
htmlstringRequired. The HTML content to deploy
titlestringOptional. Falls back to <title> tag or auto-generated name
ttlstringOptional. 1d, 7d, 30d, or never. Defaults to plan default
slugstringOptional. If provided, re-deploys to that existing site

Multi-page deploy

Parameter
Type
Description
pagesarrayRequired. Array of { path, html, title? } objects
pages[].pathstringURL path for the page, e.g. "/" or "/about"
pages[].htmlstringHTML content for this page
pages[].titlestringOptional. Page title
titlestringOptional. Site-level title
ttlstringOptional. 1d, 7d, 30d, or never
slugstringOptional. Re-deploy to existing multi-page site
json// Multi-page deploy example
{
  "pages": [
    { "path": "/", "html": "<html>Homepage</html>" },
    { "path": "/about", "html": "<html>About</html>" },
    { "path": "/contact", "html": "<html>Contact</html>" }
  ],
  "title": "My Portfolio",
  "ttl": "30d"
}

Response

json{
  "ok": true,
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "slug": "bold-fern-x3k",
  "url": "bold-fern-x3k.htmlhost.co",
  "version": 1,
  "ttl": "7d",
  "expiresAt": "2026-05-11T08:00:00.000Z"
}

GET /api/sites

Returns all your sites and aggregate usage stats.

json{
  "sites": [
    {
      "id": "...",
      "slug": "bold-fern-x3k",
      "title": "My Portfolio",
      "url": "bold-fern-x3k.htmlhost.co",
      "sizeBytes": 4200,
      "size": "4.1 KB",
      "ttl": "7d",
      "expiresAt": "2026-05-11T08:00:00.000Z",
      "createdAt": "2026-05-04T08:00:00.000Z",
      "updatedAt": "2026-05-04T08:00:00.000Z"
    }
  ],
  "usage": {
    "siteCount": 1,
    "totalBytes": 4200,
    "maxBytes": 524288000,
    "plan": "free"
  }
}

DELETE /api/sites

Delete by slug or UUID.

bashcurl -X DELETE "https://htmlhost.co/api/sites?slug=bold-fern-x3k" \
  -H "Authorization: Bearer hh_live_your_token"

GET /api/sites/{slug}/versions

The site's versions, newest first, with what each one changed. Works for the owner and collaborators.usable: falsemeans the owner's plan can't restore them.

bashcurl https://htmlhost.co/api/sites/bold-fern-x3k/versions \
  -H "Authorization: Bearer hh_live_your_token"

POST /api/sites/{slug}/versions/{n}/restore

Publishes version n again as a new version. The one it replaces stays in history. Anyone who can publish the site can restore it; if the owner's plan doesn't include restore, the response is 402 with requiresPro: true.

bashcurl -X POST https://htmlhost.co/api/sites/bold-fern-x3k/versions/12/restore \
  -H "Authorization: Bearer hh_live_your_token"

Version history

Every publish is saved as a version: each save in the editor, each deploy from the CLI, API or an AI agent, and each restore. You can see who published it, how, and which files it added, changed or removed. On Pro you can preview any earlier version, compare it with the live site, and make it live again.

Where to find it

  • Dashboard: open a site and choose the Versions tab.
  • Editor: press History on a published site. Collaborators use this one.
  • Terminal: htmlhost versions and htmlhost rollback (CLI).
  • AI assistants: the MCP server's list_versions and restore_version tools (MCP).
  • Code: the versions endpoints of the REST API.

What you can do with a version

  • Preview opens the version on its own private link, in a new tab. Links between its pages stay in that version, and visitors never see it.
  • What changed lists the files it changed since the version before. View code changes shows the line-by-line diff.
  • Compare shows what's different between that version and the live site, file by file. Images and other binary files are listed but not diffed.
  • Name a version (“Before redesign”) so it's easy to find later.
  • Edit opens the version in the editor as an unsaved draft. Nothing changes on your site until you press Update.
  • Restore makes the version live again right away.

How restore works

Restoring publishes the old version again as a new version, labelled “Restored from vN”. The version it replaces stays in history, so you can switch back the same way. A restore goes through the same checks as any publish, so it uses the owner's current plan limits.

A version holds your site's files: pages, CSS, JS and assets. Custom domains, form settings, the site password and the expiry date aren't part of a version, so a restore leaves them as they are. If you work from the CLI, run htmlhost pull after a restore so your local files match.

Plans and how long versions are kept

  • Pro: last 100 versions, kept up to 1 year (up to 200 MB of history per site). Preview, compare, name, edit and restore all work.
  • Free: your last 3 earlier versions are kept for 14 days and shown in the list, locked. Upgrade to Pro and you can restore them straight away.
  • If Pro ends: your history is kept, locked, for 30 days. Renew in that time to use it again; after that it's trimmed to the Free amount.
  • The live version is always kept. When a limit is reached, the oldest versions are removed first.
  • Version history doesn't count toward your storage quota. Files that don't change between versions are stored once.
  • The plan that counts is the site owner's. Collaborators who can publish a site can also restore it.
  • A duplicated site starts its own history at v1.

Shared header & footer

On a site with several pages, the header and footer can be shared: change them on any page and the editor asks whether to apply the change to every page, so you never have to repeat an edit page by page. Each page still highlights its own menu link.

Shared blocks carry their own styles and menu scripts where the editor can identify them, so a new page does not need matching CSS first. Keep embedded styles, font links and scripts inside the shared markers when editing code.

Share them

  1. 1In the editor, choose Shared components above the preview, or open the Shared tab. Header and Footer cards show what is shared. You can start with a single page. You can also right-click a header or footer in the preview.
  2. 2Choose Set up shared header (or footer). Preview the version from your chosen source page, then use it on all pages or choose individual pages. The list shows which copies will be replaced and where a new one will be added. Confirm with Share header on N pages.
  3. 3Choose Edit header to work on it directly, or Manage pages to change where it is used. Page selection is applied only when you save; unchecked pages keep an independent copy.

Edit a shared header or footer

In Shared components, choose Edit header or Edit footer; you can also click the Shared badge in the outline, right-click it in the preview, or use Edit Shared Header (or Footer) when something inside it is selected. The preview shows just that block, with the rest of the page hidden, so it's easy to work on: click text to edit it, drag blocks in from the Library, or use + Add sectionto add a row inside it, such as an announcement bar above the menu. Everything you add goes inside the shared block. Your edits stay a draft: they don't count as unsaved changes to your site until you press Save to N pages, which updates every page that shares the block. Done returns to the whole page. With unsaved edits, Cancel asks whether to discard them. Switching pages or publishing with an unsaved draft asks you to save it first.

When you change one

  • Apply to all pages copies your change to every page that shares it, each with its own menu link highlighted and its links adjusted for its folder.
  • Keep on this page only leaves the other pages as they were, and this page stops sharing that block.
  • Choose to do the same for later changes this session to stop being asked about that block until you close the tab. Manage pages → Advanced lets you ask again.
  • Undo works either way, and undoing never asks again.
  • If you publish while a shared block differs between pages, you're asked first.
  • New pages get the shared header and footer automatically, whether you add them in the file list or ask the AI to create them. A duplicated page keeps the ones its original had.
  • If you import or generate a site whose pages already have the same header and footer, the editor offers to share them for you.
  • Ask the AI chat to change the header and it edits it once; you then choose whether the change goes on every page, just as when you edit it yourself.

Its own styles and scripts

When you share a header or footer, the editor moves the CSS and JavaScript that only it uses out of your pages and into/shared/header.css and /shared/header.js (or footer), linked from inside the shared block. Every page that shares it then gets them, including new pages, and changing header.csschanges the header everywhere at once. It moves only what can't affect anything else: rules that can only match inside the block, and scripts that only touch it. Before anything changes, every page is rendered again; if one would look different, nothing moves and you're told which page. Styles you add while editing the header on its own move into its file when you press Save to N pages. For a block shared before this existed, openManage pages → Advanced and choose Extract styles & scripts into files.

How it works in your files

Every page keeps a full copy of the header and footer, wrapped in two HTML comments. Your pages stay plain HTML, so they work anywhere, and the CLI, downloads and version history treat them like any other page.

html<!-- hh:shared header active-class="active" -->
<header>
  <nav><a href="/">Home</a> <a href="/about" class="active">About</a></nav>
</header>
<!-- /hh:shared header -->

active-class names the class that marks the current page's link (with active-on="parent"it goes on the link's parent, such as an <li>), and aria-current="page" means that link also getsaria-current. The editor works these out when you share; you don't need to write them. Remove the comments from a page to stop sharing on it. If you edit pages outside the editor, change shared blocks on every page: htmlhost deploy warns when the copies differ, and the guide for AI agents explains the same rules to your own AI tools.

Media library

Upload images, fonts, and other assets. Each file gets a permanent URL served from Cloudflare's edge CDN. Use these URLs in your HTML.

Tip: When you htmlhost deploy, local assets are automatically uploaded to your media library and the HTML references are rewritten — no manual upload step needed. See the CLI → Automatic asset uploading section for details.

Upload via cURL

bashcurl -X POST https://htmlhost.co/api/media \
  -H "Authorization: Bearer hh_live_your_token" \
  -F "file=@logo.png"

Upload via deploy

When you run htmlhost deploy, local assets are automatically uploaded to your media library and HTML references are rewritten — no manual upload step needed. See the CLI → Automatic asset processing section for details.

Allowed file types

Images (PNG, JPG, GIF, SVG, WebP, AVIF, ICO), fonts (WOFF, WOFF2, TTF, OTF), documents (PDF), video (MP4, WebM), and audio (MP3, OGG, WAV).

AI visual editing

The browser editor includes an AI assistant that can edit your HTML visually. Click any element, describe the change in natural language, and watch it update live.

Built-in AI

Free plan: 10 AI edits a day. Pro plan: 100 AI edits a day. Uses platform-managed API keys — no setup required. Once a Free account has used all the sites its plan allows, the built-in AI works only on its live sites, not on unpublished drafts. Your own API key works anywhere.

Bring your own key (BYOK)

For unlimited edits, add your own API key from any supported provider. Keys are saved only in your browser's local storage. When you use one, it passes through our proxy to the provider and is never stored or logged.

Provider
Models
Key prefix
OpenAIGPT 5.6 Sol, GPT 5.5, GPT 4.1, GPT 4.1 Minisk-...
AnthropicClaude Opus 5.5, Claude Opus 5, Claude Sonnet 5, Claude Haiku 4.5sk-ant-...
GoogleGemini 3.1 Pro, Gemini 3.8 Flash, Gemini 2.5 Pro, Gemini 2.5 FlashAIza...

Forms

Any form on your site can deliver straight to you, with no backend and no third-party form service. Visitors' messages land in your dashboard and your inbox, and you decide what visitors see after they send.

Add a form

  • In the editor, open Library → Elements → Forms and drag a Contact Form or Email Signup onto your page. Add fields (input, text area, dropdown, checkbox, file upload) by dropping them inside the form.
  • Or ask the AI: “add a contact form with name, email and message”. Forms the AI writes are set up to deliver automatically.
  • Publish. Forms only send from your published site; in the editor preview, pressing Send shows what a visitor would see instead.

Writing your own form

Point any form at /__hh/form with method="post" and give every field a name. A hidden _form field names the form, so one site can run several (a contact form and a newsletter signup, say).

html<form action="/__hh/form" method="post">
  <input type="hidden" name="_form" value="Contact">
  <input name="name" placeholder="Your name" required>
  <input name="email" type="email" placeholder="you@example.com" required>
  <textarea name="message" required></textarea>
  <button type="submit">Send</button>
</form>

Your published site adds a small script that sends the form in the background, so visitors stay on the page. Without JavaScript the form still works: it sends normally and shows a confirmation page.

For the same spam protection as Library forms, add this hidden trap field inside the form. People never see it; bots that fill it in are discarded:

html<div aria-hidden="true" style="position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden">
  <label>Leave this field empty <input type="text" name="_hh_hp" tabindex="-1" autocomplete="off"></label>
</div>

Connect a form you already have

A form you pasted in or wrote yourself may not send anywhere yet. In the editor, click anything inside it: the AI panel shows This form doesn't send anywhere yet. Click Send to my inbox (or right-click the form → Send to my inbox). htmlhost points it at your inbox and names it for you. Publish, and it's live. Search boxes are left alone.

Once a form delivers, the same panel shows 📨, its name and how many messages it has received, with links to read them and to its settings. The 📨 also marks delivering forms in the breadcrumb and the Structure tab.

Where messages go

Open your site from the dashboard and go to Messages to read, mark as spam, or export messages to CSV. The Inbox tab on your dashboard shows messages from all your sites in one list. Each new message is also emailed to you, and replying to that email goes straight to the visitor.

Sending messages to other people

In your site's Forms tab, under Who gets new messages, click Add recipient(1 extra person on Free, up to 10 on Pro). They get one email asking them to confirm; nothing else reaches them until they do, and every email has a link to stop. They don't need an htmlhost account.

By default everyone gets every form. To split them, for example Careers to your hiring manager and Contact to sales, click All forms under a person's name and choose Only these forms. You always get everything, and you can switch your own emails off once someone else is receiving them.

You can also manage recipients from the terminal with htmlhost recipients — see the CLI section above.

How often you're emailed

In the Forms tab, Email notifications has three options. Instantly (the default) sends an email for every message. Daily summarysends one email a day, around 08:00 UTC, listing the day's messages. Paused sends nothing. Messages are always saved on the Messages tab whichever you choose.

Letting collaborators read messages

Collaborators can edit your site but can't see its messages unless you allow it. In the Forms tab, turn on Collaborators can read messages. They'll see the site in their Inbox and can read, mark and export messages; only you can delete them.

What visitors see after sending

After a successful send, a visitor either sees a confirmation message under the form or is taken to another page on your site (a “thank you” page, for example). You set this in two places:

  • For the whole site: dashboard → your site → Forms → After someone sends a form. Choose a message or a page, and optionally an error message.
  • For one form: in the editor, right-click the form (or anything inside it) → Form Settings…. Leave it on Site default, or give this form its own message or page.

A form's own setting always wins over the site default. If a form has its own message, it shows that message even when the site default is to go to a page. Pages must be on your own site and start with /; sending visitors to other websites isn't allowed. The error message, if you set one, replaces the specific reason a message couldn't be sent.

Form Settings saves your choices in the form's HTML as hidden inputs, so you can also write them by hand:

Parameter
Type
Description
_formnameThe form's name. Groups its messages in your dashboard.
_successmessageConfirmation shown under the form after sending.
_redirectpathPage on your site to open after sending, e.g. /thanks.
_errormessageShown instead of the specific reason when sending fails.
html<form action="/__hh/form" method="post">
  <input type="hidden" name="_form" value="Quote request">
  <input type="hidden" name="_redirect" value="/thanks">
  <input type="hidden" name="_error" value="Sorry, that didn't go through. Email us at hi@example.com.">
  ...
</form>

Styling the confirmation

The message appears in a <p data-hh-form-msg> added to the end of the form, withdata-state="success" or "error". It uses your page's font and colour unless you style it:

css[data-hh-form-msg] { padding: 12px 16px; border-radius: 8px; background: #ecfdf5; color: #065f46; }
[data-hh-form-msg][data-state="error"] { background: #fef2f2; color: #991b1b; }

File uploads (Pro)

Drag File Upload from Library → Elements → Forms into a form; the form is set up for files automatically. Visitors can attach up to 3 files (images, PDFs, Office documents, text or CSV). Files are checked by their content, not just their name, and kept private: you download them from the message in your dashboard, and emails only list their names.

Google Sheets (Pro)

Keep a spreadsheet of your form messages that fills itself in. Your site's Forms tab → Integrations → Google Sheetsoffers two ways. You don't connect a Google account to htmlhost for either.

Instant (recommended): each message becomes a new row within seconds.

  1. 1Create a sheet (sheets.new) and open Extensions → Apps Script.
  2. 2In htmlhost, click Copy script and paste it into Apps Script, replacing what's there. It contains your private signing secret, so don't share it.
  3. 3Click Deploy → New deployment, choose Web app, set Execute as to Me and Who has access to Anyone, then Deploy and allow access.
  4. 4Copy the Web app URL, paste it into htmlhost and click Connect & send test. A test row appears in your sheet.

New fields become new columns, rows are only ever added (so your own notes beside them stay put), and a visitor can't sneak a formula into your sheet. The script checks a signature on every request, so only htmlhost can add rows. It uses your site's single webhook slot.

Hourly link: click Create Google Sheets link and paste the =IMPORTDATA("…") formula into cell A1. Google refreshes it about once an hour. It mirrors your dashboard (read-only, oldest first), so new messages appear at the bottom. The link is shown once and works like a password: make a new one if it leaks.

Webhooks (Pro)

Send every message to your own server, or to tools like Zapier, Make or n8n. In the Forms tab → Integrations, paste an https:// URL and click Save, then Send test. Each message is POSTed as JSON a moment after it's received:

json{
  "event": "form.submission",
  "id": "5f0c…",
  "site": { "slug": "bakery", "address": "bakery.htmlhost.co" },
  "form": "Contact",
  "page": "/contact",
  "submittedAt": "2026-09-23T10:00:00.000Z",
  "email": "ada@example.com",
  "fields": [{ "name": "name", "value": "Ada" }, { "name": "message", "value": "Hi!" }],
  "files": [{ "name": "brief.pdf", "size": 48213, "type": "application/pdf" }]
}

To check a request really came from htmlhost, compare the X-Htmlhost-Signature header with an HMAC-SHA256 of the raw body, using the signing secret shown in the Forms tab:

jsconst expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers["x-htmlhost-signature"]));

Your endpoint should reply within 5 seconds. Redirects aren't followed, and only public addresses can be used. The Forms tab shows the result of the latest delivery. Attachments are listed by name only; download them from the dashboard.

Using AI with forms

If you build locally with an AI coding tool, run htmlhost agents in your project first. It tells your AI exactly how forms work here, so the forms it writes deliver to you.

The AI knows how forms work here. Try “after this form is sent, go to the thanks page”, “change the confirmation to ‘We'll reply within a day’”, or “make the form's success message green”. In Discuss mode you can also just ask how forms work.

Spam protection & safety

  • Every delivered form includes a hidden honeypot field and a fill-time check; caught bots are discarded or filtered into Spam, which is cleared after 30 days
  • Each visitor can send 5 messages per site every 10 minutes, and 20 per hour across all sites
  • Forms can only be sent from your own site, and password-protected sites must be unlocked first
  • Fields that ask for passwords, card numbers or similar details are rejected, to protect your visitors from phishing

Limits

Free accounts receive up to 50 messages a month across all sites; Pro receives up to 5,000. We email you once if you reach the limit. File attachments are a Pro feature: up to 3 files per message, 10 MB each and 25 MB in total (images, PDFs, Office documents, text and CSV), with 2 GB of attachments a month.

Custom domains

Serve any site from your own domain. Custom domains are included with Pro, or you can buy a domain add-on for a single domain, billed yearly. Go to Settings → Domains, enter the domain and choose the site it should show. You can also start from a site in your dashboard: ⋮ → Add custom domain.

DNS setup

After you add the domain, the Domains page shows the exact record to create. Copy it from there, because the value can differ from domain to domain. It will be one of these two:

Parameter
Type
Description
example.comARoot (apex) domain. Name: @, value: the IP address shown (usually 76.76.21.21).
www.example.comCNAMEAny subdomain. Name: the subdomain part (www, blog, shop.eu…), value: the hostname shown (usually cname.vercel-dns.com).
text# Root domain (example.com)
Type:  A
Name:  @
Value: 76.76.21.21        ← use the value shown in Settings → Domains

# Subdomain (www.example.com)
Type:  CNAME
Name:  www
Value: cname.vercel-dns.com   ← use the value shown in Settings → Domains
  • Remove any other A, AAAA or CNAME records for the same name first (for example, your registrar's parking page). They conflict with the new record.
  • A root domain can't use a CNAME at most providers, so use the A record for it.
  • On Cloudflare, set the record to DNS only (grey cloud), not Proxied, so the certificate can be issued.
  • We detect your DNS provider and link to its dashboard and instructions on the Domains page.
  • Want both example.com and www.example.com? Add each as its own domain, one A record and one CNAME.

Verification & HTTPS

The Domains page checks your DNS automatically and moves through Awaiting DNS → Provisioning TLS → DNS & TLS ready. The HTTPS certificate is issued for you, with nothing to upload. Most DNS changes are picked up within minutes, but some networks can take a few hours (rarely up to 24) to see the new record, so the domain may work on one connection before another.

A site on an active custom domain doesn't expire while the domain stays connected. Forms and password protection work on custom domains exactly as they do on your htmlhost.co address.

API tokens

Tokens authenticate CLI and API requests. Generate them at Settings → API keys.

Token format

texthh_live_a91fc7b3d2e014c8f5a6b9d0e1f2a3b4c5d6e7f8

Tokens start with hh_live_ and contain 48 hex characters. The raw token is shown once at creation. We store only the SHA-256 hash on our servers.

Security

  • Tokens are hashed server-side — we never store the raw token
  • ~/.htmlhostrc is created with 0600 permissions (owner-only)
  • Revoke a token instantly from Settings — all sessions using it stop immediately
  • Maximum 10 tokens per account

Plans & limits

Limit
Free
Pro ($5/mo)
Sites3 in totalUnlimited
Site lifetimeUp to 30 daysNever expires
Storage500 MB10 GB
AI edits10 a day100 a day
Custom domains$5/yr eachUnlimited, included
Form messages50 a month5,000 a month
Form email recipients1 per siteUp to 10 per site
Form file attachments—✓
Password-protected sites—✓
Collaborators—Up to 5 per site
Media library250 MB (50 MB per file)10 GB (5 GB per file)
File transfersUp to 1 GBUp to 5 GB
Version history and restore—Last 100 versions, kept up to 1 year
Expired site restore—✓
API / CLI✓✓

What happens when a site expires?

When a site's TTL elapses, the public URL goes offline (returns a 410). The site stays in your dashboard so you don't lose track of it.

  • Free publishing is limited; deleting or expiring sites does not renew free publishing access
  • Free API/CLI publishing requires one-time browser verification at /new; contact support if unavailable
  • Free users can upgrade to Pro to restore expired sites
  • Pro users can still preview expired sites and republish with one click to bring them back online
  • Every site you create counts toward the free allowance for good, even after it expires or is deleted

Questions? Reach out at hey@htmlhost.co