Everything you need to deploy, manage, and edit HTML sites from the browser, terminal, or API.
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.
Go to htmlhost.co, paste or drop an HTML file, pick a TTL, and hit Publish. Done.
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)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"}'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 --versionAuthenticate 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.
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.
--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)--ttl <value>string--slug <slug>string--title <title>string--newflag--no-assetsflag--no-pull-checkflag--jsonflagDeploy 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 TTLAdd 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.
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-assetsHow 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 uploadedurl() in <style> blocks, inline styles, and external CSS files — assets uploaded, URLs rewrittenAfter 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 --newBuilding 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 aloneTools that can browse can also read the guide directly at htmlhost.co/agents.md.
List all your sites in a formatted table showing slug, URL, size, TTL, and expiry.
Delete a site by slug. Use --force to skip the confirmation prompt.
Open a deployed site in your default browser.
bashhtmlhost open bold-fern-x3kShow the authenticated user's email, handle, and plan.
Remove saved credentials from ~/.htmlhostrc.
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 invitationUse --force to skip the confirmation prompt on remove, and --json for machine-readable output.
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.
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.
--jsonflagOutput the status as JSON--jsonflagbashhtmlhost 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)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.
--force, -fflagPull without confirmation prompt--dry-runflagShow what would change without writing files--force, -fflag--dry-runflagbash# 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.
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 editsUse --force to overwrite an existing non-empty directory.
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).
All endpoints accept Authorization: Bearer <token> headers. Generate tokens at Settings → API keys.
Base URL: https://htmlhost.co
/api/sites/api/sites/api/sites?slug={slug}/api/sites/pull?slug={slug}/api/media/api/media/api/media/api/tokens/api/tokens/api/tokens?id={id}/api/tokens/meCreate a new site or update an existing one. Supports both single-page and multi-page deploys.
htmlstringRequired. The HTML content to deploytitlestringOptional. Falls back to <title> tag or auto-generated namettlstringOptional. 1d, 7d, 30d, or never. Defaults to plan defaultslugstringOptional. If provided, re-deploys to that existing sitehtmlstringtitlestringttlstringslugstringpagesarrayRequired. Array of { path, html, title? } objectspages[].pathstringURL path for the page, e.g. "/" or "/about"pages[].htmlstringHTML content for this pagepages[].titlestringOptional. Page titletitlestringOptional. Site-level titlettlstringOptional. 1d, 7d, 30d, or neverslugstringOptional. Re-deploy to existing multi-page sitepagesarraypages[].pathstringpages[].htmlstringpages[].titlestringtitlestringttlstringslugstringjson// 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"
}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"
}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 by slug or UUID.
bashcurl -X DELETE "https://htmlhost.co/api/sites?slug=bold-fern-x3k" \
-H "Authorization: Bearer hh_live_your_token"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.
bashcurl -X POST https://htmlhost.co/api/media \
-H "Authorization: Bearer hh_live_your_token" \
-F "file=@logo.png"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.
Images (PNG, JPG, GIF, SVG, WebP, AVIF, ICO), fonts (WOFF, WOFF2, TTF, OTF), documents (PDF), video (MP4, WebM), and audio (MP3, OGG, WAV).
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.
Free plan: 10 AI edits per day. Pro plan: 100 edits per day. Uses platform-managed API keys — no setup required.
For unlimited edits, add your own API key from any supported provider. Keys are stored in your browser's localStorage and never sent to our servers.
sk-...sk-ant-...AIza...sk-...sk-ant-...AIza...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.
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>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.
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.
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.
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.
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.
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:
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:
_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._formname_successmessage_redirectpath_errormessagehtml<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>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; }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.
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.
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.
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.
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.
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.
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.
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:
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).example.comAwww.example.comCNAMEtext# 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 → DomainsA, AAAA or CNAME records for the same name first (for example, your registrar's parking page). They conflict with the new record.A record for it.example.com and www.example.com? Add each as its own domain, one A record and one CNAME.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.
Tokens authenticate CLI and API requests. Generate them at Settings → API keys.
texthh_live_a91fc7b3d2e014c8f5a6b9d0e1f2a3b4c5d6e7f8Tokens 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.
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.
Questions? Reach out at hey@htmlhost.co