HaveTML Docs
Publish, share, and automate your artifacts
HaveTML gives single-file HTML and Markdown a permanent link, a project home, feedback tools, and API access for browsers, scripts, and AI tools.
Upload
Drop HTML or Markdown into the dashboard.
Share
Send the artifact or project link to readers.
Automate
Use REST, MCP, or the Chrome extension.
01
Start with a link
- Create an account, or sign in if you already have one.
- Open the Dashboard and drop a
.html,.htm,.md, or.markdownfile into the upload zone. - Open the artifact link at
/f/:slug, then share it with anyone who should read it.
Markdown files render as readable documents. HTML files run in a sandboxed viewer so the artifact cannot access the HaveTML app origin.
02
Manage the record
The dashboard is where owners organize projects, update files, and manage reader context. Re-uploading a new version keeps the artifact link stable while preserving version history.
- Projects: group artifacts under a public
/p/:slugpage with a Markdown description. - Feedback: collect comments and pins from signed-in readers, or turn comments off per artifact.
- Reader context: add Markdown notes, links, and supporting Markdown docs beside an artifact.
- Access controls: add a password or expiry date when a share link should be limited.
03
Create an API token
API tokens let the extension, MCP server, and your own scripts upload artifacts. Create and revoke tokens from Dashboard API access. The full token is shown once, so store it somewhere private.
Authorization: Bearer havetml_your_token_here04
REST API
Send JSON requests with a bearer token. File uploads accept filename, content, optional title, optional project slug, and optional visibility. Artifacts may be up to 50 MB; this REST route still hops Vercel, which rejects bodies over 4.5 MB — use the dashboard for larger files. Re-uploading the same title or filename into the same project creates a new version.
New artifacts are private unless you say otherwise — the URL in the response opens for you and anyone you invite, and for nobody else. Pass "visibility": "unlisted" for a link anyone can follow, or "public"to also list it in Explore. Leave it out and the destination project's default applies. Every response carries the visibility the artifact landed on.
To change an artifact that already exists, PATCH its visibility. An upload that versions an existing artifact leaves its level alone unless the body names one, so this is the endpoint for “now share it” and for taking a link back. Owner only, and an artifact carrying a password or a link expiry cannot become public until those are cleared.
To read an artifact back, GET its content. You get the source — the HTML, Markdown or SVG the page is built from — for the current version, or for an earlier one with ?v=. The token reads exactly what you could read signed in as yourself: your own artifacts, ones shared with you, ones shared with a circle you are in, and any public or unlisted artifact whose slug you have. An artifact you may not read answers 404, the same as one that does not exist. An end-to-end encrypted artifact answers 409 — HaveTML holds no key, so there is no source to return — and one over 4.5 MB answers 413 with its real size.
curl https://havetml.com/api/v1/artifacts/a1b2c3d4/content \
-H "Authorization: Bearer havetml_your_token_here"
# → { "slug": "a1b2c3d4", "title": "Dashboard",
# "content": "<!doctype html><html>...</html>",
# "content_type": "html", "version_number": 4, "size_bytes": 10240 }curl -X PATCH https://havetml.com/api/v1/artifacts/a1b2c3d4/visibility \
-H "Authorization: Bearer havetml_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "visibility": "unlisted" }'
# → { "slug": "a1b2c3d4", "visibility": "unlisted",
# "previous_visibility": "private",
# "url": "https://havetml.com/f/a1b2c3d4" }curl -X POST https://havetml.com/api/v1/files \
-H "Authorization: Bearer havetml_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"filename": "dashboard.html",
"content": "<!doctype html><html>...</html>",
"title": "Dashboard",
"project": "launch-notes",
"visibility": "unlisted"
}'GET /api/v1/files
GET /api/v1/files?project=launch-notes
POST /api/v1/files
GET /api/v1/projects
POST /api/v1/projects
PATCH /api/v1/artifacts/:slug/visibility
GET /api/v1/artifacts/:slug/content
GET /api/v1/artifacts/:slug/content?v=3
GET /api/v1/artifacts/:slug/comments
POST /api/v1/artifacts/:slug/comments
GET /api/v1/artifacts/:slug/collaborators
POST /api/v1/artifacts/:slug/collaborators
DELETE /api/v1/artifacts/:slug/collaborators
GET /api/v1/artifacts/:slug/relations
POST /api/v1/artifacts/:slug/relations
DELETE /api/v1/relations/:id
GET /api/v1/projects/:slug/members
POST /api/v1/projects/:slug/members
DELETE /api/v1/projects/:slug/members
POST /api/v1/projects body:
{
"name": "Launch notes",
"description": "Markdown shown on the public project page"
}
PATCH /api/v1/artifacts/:slug/visibility body:
{
"visibility": "unlisted" // private, circle (Team), unlisted or public
}
POST /api/v1/artifacts/:slug/comments body:
{
"body": "Comment text",
"anchor": { /* optional pin location */ },
"parent_id": "uuid", // optional for threaded replies
"version": 1 // optional, defaults to current
}
POST /api/v1/artifacts/:slug/collaborators body:
{
"email": "editor@example.com",
"role": "editor" // "viewer" or "editor", defaults to "viewer"
}
DELETE /api/v1/artifacts/:slug/collaborators body:
{
"email": "editor@example.com"
}
POST /api/v1/artifacts/:slug/relations body:
{
"to_slug": "Ab12Cd34Ef",
"rel": "dependsOn", // relatesTo (default), dependsOn, supersedes, references
"label": "needs the Q3 numbers first" // optional, up to 120 characters
}
// A relation is metadata only — it gives nobody access to either artifact.
// Rows with source "canvas" were drawn on a board and are read-only here:
// DELETE answers 409 and the line comes off on the canvas instead.
POST /api/v1/projects/:slug/members body:
{
"email": "member@example.com",
"role": "editor" // "viewer" or "editor", defaults to "viewer"
}
DELETE /api/v1/projects/:slug/members body:
{
"email": "member@example.com"
}| Error | What to do |
|---|---|
| missing_token | Add Authorization: Bearer havetml_... |
| invalid_token | Create a new token or check the one you sent. |
| project_not_found | Use an existing project slug. |
| unsupported_type | Upload .html, .md, .yaml, .json, or .jsx/.tsx. |
| too_large | Keep single-file artifacts under 50 MB. MCP and the REST API still hop Vercel, which rejects bodies over 4.5 MB — upload larger files from the dashboard. |
| empty_content | Send a non-empty content string. |
| name_required | Send a non-empty project name. |
| invalid_visibility | Send private, circle, unlisted or public. Required when changing an existing artifact. |
| not_authorized | Only an artifact's owner can change who can see it. |
| password_set | Clear the artifact's password before making it public — everyone has to be able to read it. |
| expiry_set | Set the link expiry to never before publishing. Published work doesn't expire. |
| encrypted_cannot_be_public | An encrypted artifact can't be listed. Unlisted is as open as it gets. |
| invalid_rel | Relate artifacts with relatesTo, dependsOn, supersedes or references — put anything else in the label. |
| label_too_long | Keep a relation label under 120 characters. |
| self_link | An artifact can't be related to itself. |
| duplicate | That relation already exists between those two artifacts. |
| canvas_derived | This relation is a line someone drew on a canvas. Remove the line on the board. |
05
MCP for AI tools
Signed in? The connect wizard does this for you: pick your tool, copy one prefilled config, and check the connection. The configs below are the same ones it writes.
Connecting gives your assistant the tools. The HaveTML skill is what tells it when to use them — a single markdown file, served at /skill.md, that covers artifacts, state, visibility, notes, decisions and canvases. Install it in Claude, Claude Code or Cursor, or paste it into anything else.
HaveTML exposes a Streamable HTTP MCP endpoint at /api/mcp. Use a dashboard token in the Authorization header. Set up whichever client you use:
Claude asking for OAuth?
If Claude shows an Add custom connector form with OAuth Client ID and OAuth Client Secret, paste https://havetml.com/api/mcpas the remote MCP server URL and leave both OAuth fields blank. Claude will discover HaveTML's OAuth flow and redirect you here to approve access.
For local-only Claude Desktop setup, you can still use Desktop app settings, then Developer, then Edit Config. That path uses a localmcp-remotebridge instead of Claude's cloud connector.
Claude connector
Name: HaveTML
Remote MCP server URL: https://havetml.com/api/mcp
OAuth Client ID: (leave blank)
OAuth Client Secret: (leave blank)Claude → Customize → Connectors → Add custom connector. Paste the URL, leave OAuth Client ID/Secret blank, then approve access on HaveTML.
Claude Code
claude mcp add --transport http havetml https://havetml.com/api/mcp \
--header "Authorization: Bearer havetml_your_token_here"Run in any terminal, then type /mcp in Claude Code to confirm it connected.
Codex
# ~/.codex/config.toml
[mcp_servers.havetml]
url = "https://havetml.com/api/mcp"
bearer_token_env_var = "HAVETML_TOKEN"
# then, in your shell profile:
export HAVETML_TOKEN="havetml_your_token_here"Add to ~/.codex/config.toml. ChatGPT desktop, Codex CLI, and the Codex IDE extension share this config.
Cursor
{
"mcpServers": {
"havetml": {
"url": "https://havetml.com/api/mcp",
"headers": {
"Authorization": "Bearer havetml_your_token_here"
}
}
}
}Global: ~/.cursor/mcp.json — or .cursor/mcp.json inside a project.
Windsurf
{
"mcpServers": {
"havetml": {
"serverUrl": "https://havetml.com/api/mcp",
"headers": {
"Authorization": "Bearer havetml_your_token_here"
}
}
}
}Add to ~/.codeium/windsurf/mcp_config.json, or paste it under Settings → Cascade → Manage MCPs → View raw config. Remote servers need serverUrl, not url; hit Refresh afterwards.
Claude Desktop
{
"mcpServers": {
"havetml": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://havetml.com/api/mcp",
"--header",
"Authorization:${AUTH_HEADER}"
],
"env": {
"AUTH_HEADER": "Bearer havetml_your_token_here"
}
}
}
}Use Desktop app → Developer → Edit Config, not Customize → Connectors. This uses mcp-remote as a local bridge; restart Claude Desktop after saving.
Other MCP clients
{
"mcpServers": {
"havetml": {
"url": "https://havetml.com/api/mcp",
"headers": {
"Authorization": "Bearer havetml_your_token_here"
}
}
}
}Use this for tools that support Streamable HTTP remote servers. For stdio-only clients, use the Claude Desktop bridge pattern.
list_templates: browse the templates — working artifacts, with their state, ready to copy.get_template: read one template’s starter prompt, its demo link, and the state that demo runs on.use_template: copy a template into your account as a private artifact, seeded with its state.upload_artifact: upload HTML or Markdown and get a permanent link. Stored cap is 50 MB; this MCP transport rejects bodies over 4.5 MB.create_project: create a shareable project page.list_projects: list project slugs, names, and file counts.list_artifacts: list artifacts, optionally scoped to one project slug, folder, or category.category="skill"narrows it to your own skills, including private and unlisted ones.search_skills: search published skills — Markdown artifacts whose source is a SKILL.md an agent can install and load. Public skills only, anyone's; no token needed. Omit the query to browse the newest.get_artifact_content: read an artifact's source — the HTML, Markdown or SVG behind the page — for the current version or an earlier one. You can read what you could read signed in as yourself: your own artifacts, ones shared with you, ones shared with a circle you are in, and any public or unlisted artifact whose slug you have. Encrypted artifacts return no content.set_artifact_visibility: change who can see an artifact that already exists — private, circle, unlisted or public. Owner only.set_project_default_visibility: choose what new artifacts filed into a project inherit. Never moves anything already in it.update_artifact_json: change an artifact's live JSON state — kanban columns, checklist ticks, dashboard numbers — without re-uploading its content or cutting a new version. Owner and editors.add_relation: link two artifacts —relatesTo,dependsOn,supersedesorreferences, plus an optional label. A relation is metadata; it grants no access to either artifact.list_relations: list relations for one artifact, one project, or everything. A relation is listed only when you can read both artifacts it joins.remove_relation: delete one relation by id. Lines drawn on a canvas are the board's — remove those on the canvas instead.add_comment: add a comment to an artifact, with optional anchor or parent_id for threaded replies.list_comments: list all comments on an artifact.list_notes,add_noteandupdate_notes: the Notes tab in an artifact's sidebar. One markdown document per artifact, readable by anyone who can open it, writable by its owner.invite_collaborator: invite someone by email to view or edit an artifact. A new invite emails them the link.list_collaborators: list collaborators with access to an artifact.revoke_collaborator: revoke access to an artifact.invite_project_member: invite someone to a project, emailing them the link. Project editors inherit edit access to all artifacts in that project.list_project_members: list members of a project.revoke_project_member: revoke project access.list_events: poll for changes to your artifacts — JSON state updates and new comments.register_webhook: get those same changes pushed to an HTTPS endpoint instead of polling.list_webhooksanddelete_webhook: manage registered webhooks.
Knowing when an artifact changed
People edit artifacts after you hand them over: they tick things off an interactive checklist, fill in a form, leave a comment. Every one of those writes appends an event you can read back, either by polling or by having it pushed to you.
list_events is the simple path. Call it with no arguments for everything you own, or with a slug for one artifact. Each response carries a cursor; pass it back as sincenext time and you get only what has happened since. Events are summaries — the slug, which top-level JSON keys changed, a comment's id and the first 280 characters of its body — so follow up with get_artifact when you need the full state.
Verifying a webhook delivery
register_webhook returns a signing secret once, at registration. Every delivery is a JSON POST carrying X-HaveTML-Signature: the hex HMAC-SHA256 of the raw request body under that secret. Compute it over the bytes you received, before parsing, and compare with a constant-time check.
Delivery is best-effort and retried with backoff, so treat it as at-least-once: the same id can arrive twice, and events are not guaranteed to arrive in order. Reply 2xx quickly and do the work afterwards.
POST https://your-endpoint.example.com/havetml
X-HaveTML-Signature: 9f86d081884c7d65...
X-HaveTML-Event: json_updated
{
"id": 1284,
"type": "json_updated",
"slug": "a1b2c3d4",
"file_id": "3f1e...",
"created_at": "2026-09-04T12:00:00.000Z",
"data": {
"slug": "a1b2c3d4",
"title": "Launch checklist",
"size_bytes": 412,
"previous_size_bytes": 388,
"changed_keys": ["tasks"]
}
}06
Chrome extension
The extension uploads HTML or Markdown from the browser toolbar and copies the share link to your clipboard.
Install HaveTML from the Chrome Web Store ↗
- Install the extension from the Chrome Web Store and pin it to your toolbar.
- Open the HaveTML extension settings and paste an API token from Dashboard API access.
- Pick a project if needed, then drop a
.htmlor.mdfile.
Re-uploading a file with the same name into the same project creates a new version.