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

  1. Create an account, or sign in if you already have one.
  2. Open the Dashboard and drop a.html, .htm, .md, or .markdown file into the upload zone.
  3. 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/:slug page 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_here

04

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"
}
ErrorWhat to do
missing_tokenAdd Authorization: Bearer havetml_...
invalid_tokenCreate a new token or check the one you sent.
project_not_foundUse an existing project slug.
unsupported_typeUpload .html, .md, .yaml, .json, or .jsx/.tsx.
too_largeKeep 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_contentSend a non-empty content string.
name_requiredSend a non-empty project name.
invalid_visibilitySend private, circle, unlisted or public. Required when changing an existing artifact.
not_authorizedOnly an artifact's owner can change who can see it.
password_setClear the artifact's password before making it public — everyone has to be able to read it.
expiry_setSet the link expiry to never before publishing. Published work doesn't expire.
encrypted_cannot_be_publicAn encrypted artifact can't be listed. Unlisted is as open as it gets.
invalid_relRelate artifacts with relatesTo, dependsOn, supersedes or references — put anything else in the label.
label_too_longKeep a relation label under 120 characters.
self_linkAn artifact can't be related to itself.
duplicateThat relation already exists between those two artifacts.
canvas_derivedThis 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, supersedes or references, 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_note and update_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_webhooks and delete_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 ↗

  1. Install the extension from the Chrome Web Store and pin it to your toolbar.
  2. Open the HaveTML extension settings and paste an API token from Dashboard API access.
  3. Pick a project if needed, then drop a .html or .md file.

Re-uploading a file with the same name into the same project creates a new version.