htmlsharechecking
/ docs

the whole api,
one page.

htmlshare turns an html document into a public url. one endpoint to publish, one mcp server for agents.

to publish
one POST
for agents
one mcp server
to get started
no sdk, no account

publish in one request.

01 / quickstart
terminal
curl -X POST https://api.htmlshare.net/v1/publish \
-H "content-type: application/json" \
-d '{"html":"<!doctype html><h1>hello</h1>"}'
200 OK
{
"siteId": "k9m2qa42",
"url": "https://htmlshare.net/p/k9m2qa42",
"expiresAt": "2026-09-14T12:00:00.000Z",
"title": null,
"manageToken": "mt_...",
"claimUrl": "https://htmlshare.net/claim?siteId=k9m2qa42&manageToken=mt_...",
"notice": {
"type": "expiration",
"message": "This anonymous page expires 24 hours after publishing. ..."
}
}
url
a live page, immediately
expiresAt
anonymous pages go in 24h
manageToken
shown once — keep it

the token lets you edit the page in place instead of publishing a second url. claimUrl is the one-click version of keeping it: opening it claims the page into an account and makes it permanent.

never put claimUrl in the published page itself — anyone holding it owns the page. if you publish on someone’s behalf, show it to that person.

the publish endpoint.

02 / publish api
POST/v1/publish
json body
application/json with an html string field
file upload
multipart/form-data with a file field — text/html, or ending in .html / .htm
max size
10 mb, either way
slug (key only)
lowercase alphanumeric plus hyphens, 2–64 chars, unique per account. republishing one returns 409 conflict

with an api key, publish at a memorable url under your claimed prefix:

terminal
curl -X POST https://api.htmlshare.net/v1/publish \
-H "authorization: Bearer ps_live_..." \
-F "slug=q3-report"
200 OK
{
"siteId": "x7p4nn21",
"url": "https://htmlshare.net/@yourprefix/q3-report",
"expiresAt": null,
"title": "Q3 Report"
}
anonymous
htmlshare.net/p/{siteId}
expires in 24h
with a key
htmlshare.net/@{prefix}/{slug}
never expires

the manage token.

03 / manage a page

an anonymous publish mints one manageToken (prefix mt_) and returns it once. send it as an x-manage-token header. a page published with an api key gets no token and needs none — the same key authorizes everything below.

the token is never retrievable again, and it is the only proof of ownership an unclaimed page has. we cannot look it up, resend it, or verify it for you.

editing beats republishing. a second publish mints a second url and orphans the first — the one thing a link you already sent cannot survive.

terminal
# read back exactly what is stored (no badge, no rewriting)
curl https://api.htmlshare.net/v1/sites/k9m2qa42/content \
-H "x-manage-token: mt_..."
# update the document, keeping the same url
curl -X PUT https://api.htmlshare.net/v1/sites/k9m2qa42/content \
-H "x-manage-token: mt_..." \
-H "content-type: text/html" \
--data-binary @page.html
# add a file the page can reference, instead of inlining base64
curl -X PUT https://api.htmlshare.net/v1/sites/k9m2qa42/files/logo.png \
-H "x-manage-token: mt_..." \
-H "content-type: image/png" \
--data-binary @logo.png
# -> https://htmlshare.net/p/k9m2qa42/logo.png
# list what the page is holding, with the quota left
curl https://api.htmlshare.net/v1/sites/k9m2qa42/files \
-H "x-manage-token: mt_..."
# ask us to email the claim link before the page expires
# (this one takes the token in the body, not the header)
curl -X POST https://api.htmlshare.net/v1/sites/k9m2qa42/reminder \
-H "content-type: application/json" \
-d '{"manageToken":"mt_...","email":"[email protected]"}'
the token can
read the page
update the document
upload, list, delete its files
attach a reminder email
the token cannot
change a slug (needs a key)
see any other page
touch a page owned by an account

deleting an anonymous page outright is an agent path today: the mcp delete_site tool takes the token; DELETE /v1/sites/{siteId} over rest is dashboard (session) only.

lifecycle

/1
public for 24 hours.
/2
then it answers 410.
/3
recoverable by its creator for 7 more days.
/4
claim it in that window and it is restored at the same url, permanently.
claiming ends the token.
the page belongs to an account now, so later edits go through its api key or the dashboard. an agent holding a token should expect its next call to be a 404 — that is the page changing hands, not vanishing. a lost token has no recovery path.

bearer keys.

04 / api keys
HEADERauthorization: Bearer ps_live_…
where
the dashboard, under api keys. shown once at creation.
what it gets you
pages tied to your account, no expiry, the higher rate limit
works on
POST /v1/publish, the mcp connector, and every endpoint in manage a page for sites it owns — including PATCH /v1/sites/{siteId}/slug, which no manage token can use
invalid key
a hard 401, not an anonymous fallback
still dashboard-only
listing your sites, and deleting them over rest

what your plan covers

publishing is free and stays free. every endpoint on this page works on a free account, over rest, mcp and a2a alike, with nothing metered by plan. a few capabilities beyond publishing are lifetime pro, a one-time purchase: pages served without the attribution badge, password-protected pages (set in the dashboard), and a bigger per-site asset allowance — 10 files / 10 mb free, 50 files / 100 mb paid. custom domains, version history, advanced analytics and hosted page storage are part of what the plan will include and are not built yet.

GET/v1/entitlements
what it answers
what this account may do, as booleans and numbers — not a plan name. a lifetime purchase, a subscription and a staff grant can all be in effect at once, so no single string describes an account
who can ask
an api key, an oauth token, or a session cookie. there is no anonymous answer — an anonymous caller holds no grants, and a client that cached that would have the wrong answer for someone who does
elsewhere
mcp tool get_entitlements, a2a method htmlshare/entitlements. both flag the capabilities the plan grants that nothing implements yet
when refused
402 upgrade_required, carrying the capability and where to upgrade. deliberately not 403 — the request was well formed and the credential was valid, so retrying it differently cannot work

publish from claude or chatgpt.

05 / mcp connector
MCPhttps://api.htmlshare.net/v1/mcp

connect it and ask your agent to “publish this as an htmlshare link”. streamable http.

an account is required
this connector needs a credential. a request without one answers 401 with an oauth challenge, so your client offers a sign-in when you connect it — nothing to paste or store. an existing ps_live_ key works too.
what you get
permanent pages, tied to your account, listed in your dashboard, counted against your own limit (1000 / 24h). no expiry, no manageToken, nothing to claim. publishing without an account is still one POST away over rest.

tools

publish_html
a new page. returns its siteId.
get_html
reading a page back. trimmed by default — script and style bodies and inline images collapse to size markers — or just the elements matching a css selector.
patch_html
editing. exact, unique string replacements, all-or-nothing; the response is the new size, not the document — a one-line change costs a few dozen tokens instead of two copies of the page.
update_html
full rewrites only.
set_slug
renames a page to htmlshare.net/@{prefix}/{slug}, or clears it. no redirects — the old url stops working.
list_sites
the pages on your account, newest first, with their urls and site ids. the only tool besides publish_html that hands back a site id, so it is how an agent acts on a page it did not just publish. account credential only — anonymous pages are held by a manage token, not an account.
delete_site
permanent takedown. the document and its metadata are erased and the url does not come back — the one tool with no undo.
get_entitlements
what the account may do. for a capability that might be paid — never before a publish, which is free.

every tool but publish_html needs the siteId plus the credential that page answers to: its manage token while it is anonymous, or the publishing key once it is owned. documents over 500KB are rejected rather than truncated.

getting a file onto a page

three ways, and the size decides which.

put_asset
base64 in the tool call
the bytes pass through the model — a 70KB image is ~32,000 tokens. keep it for favicons and small art.
put_asset_from_url
server fetches it
hand it a public https url — one short call, whatever the file size.
create_upload
single-use link, 15 min
carries no credential, so an agent can hand you a curl for a file that only exists on your machine.

all three accept a replace string: the file is stored and every occurrence of that string in the page is swapped for its url in the same call — how a placeholder like __OG_IMAGE__ gets filled without a second round trip. publishes that inline images over 4KB get this done automatically. list_files shows what a page holds and how much quota is left; delete_file frees one. publishing with a placeholder still in a src, href or content attribute returns a warning naming it — the page is live either way; we never change what you stored.

setup, per client

settings → connectors → add custom connector. name it htmlshare, url https://api.htmlshare.net/v1/mcp. the dialog detects that authentication is required; for the oauth client, choose no client id — register one automatically.

claude sends you to sign in, you approve the connection once, and it holds and refreshes the token from then on. prefer a key? choose none for authentication and add a header Authorization: Bearer ps_live_... (the bare key works too).

social cards.

06 / social cards
GET/v1/og?title=…

a page with no og: tags pastes into slack, imessage, discord and x as a bare link. we never inject tags into your html — the bytes you publish are the bytes we serve — so the tags go in the document you author. this endpoint renders the picture they point at: a branded 1200×630 png, drawn from a title in the query string.

head
<!-- in the <head> of the document you publish -->
<meta property="og:title" content="Q3 Revenue Report" />
<meta property="og:description" content="Pipeline, bookings and net retention." />
<meta property="og:image"
content="https://api.htmlshare.net/v1/og?title=Q3%20Revenue%20Report" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" />
no key, no siteId
keyed on the title, not the page — so you can write the whole <head> before the first request.
title
url-encoded, under 120 characters. longer is truncated with an ellipsis, missing renders a generic branded card.
what it draws
text only, in our own type and colours. not a screenshot of your page, and it never loads anything your title names.
caching
same title, same image — long-lived cache-control: immutable and an etag, so crawlers are answered at the edge. rate limited per ip.

for agents that read specs.

07 / openapi

the whole rest surface is openapi 3.1 at api.htmlshare.net/openapi.json (the apex serves the same file). every operation has an operationId, a description and typed responses, so it drops straight into anything that turns a spec into tool definitions. there is no generated copy anywhere: the worker serves the same object it routes from.

custom gpt action

/1
in the gpt editor: configure → create new action → import from url, and paste the spec url. the importer reads the first servers entry, so calls go to api.htmlshare.net.
/2
leave authentication on none for anonymous publishes — real urls, no account, gone in 24 hours. or set api key, auth type bearer, and paste a ps_live_ key.
/3
a gpt you intend to publish also wants a privacy policy url: use htmlshare.net/privacy.

the json operations an action can drive:

publishHtmlgetSiteContentupdateSiteContentsetSiteSluglistSiteAssetsreportAbusegetHealth

uploadSiteAsset and redeemUploadLink are described for completeness but expect an application/octet-stream body, which a gpt action has no way to send. to get an image onto a page from an agent, use the mcp connector’s put_asset_from_url, or call the rest endpoint from a script.

an action has nowhere durable to keep the manageToken and claimUrl an anonymous publish returns. have the gpt print them in its reply — and never let it write the claim url into the published html.

other discovery endpoints

/.well-known/api-catalog
rfc 9727 linkset pointing at the spec, these docs and health.
/llms.txt
plain-text orientation for an agent that found us by crawling.
/auth.md
the credentials, described.
/.well-known/agent-card.json
the a2a card. POST /v1/a2a publishes over the a2a json-rpc binding with the same one skill the card advertises.

your page, your domain.

08 / custom domains

a lifetime pro page can serve on a domain you own, over https, with a certificate we issue and renew. you add the domain in the page’s settings, create one dns record at your provider, and the page answers on it a few minutes later. the original htmlshare url keeps working — a custom domain is a second address, never a rename.

use a subdomain

go.yourdomain.com works. yourdomain.com usually does not, and the reason is dns rather than us: pointing a name at us means a CNAME record, and the dns standard forbids a CNAME at the root of a domain, because the root already carries the SOA and NS records that say where the domain lives. a name cannot be both.

the practical version: www, go, app, book — any label in front of your domain is fine. the bare domain needs the workaround below, and whether it is available depends on who runs your dns, not on your plan here.

when the bare domain does work

some dns providers implement a non-standard record that behaves like a CNAME at the root and resolves it for you — cloudflare calls it cname flattening, route 53 calls it an ALIAS, others call it ANAME. if your domain is at one of those, the bare domain can point here like any subdomain. if it is at a registrar that only offers plain A and CNAME records, it cannot, and no setting on our side changes that.

moving your dns to cloudflare

this is the usual way to get a bare domain working, and it is free. you move the domain’s dns hosting to your own cloudflare account — the domain stays yours, your registrar stays your registrar, and cloudflare answers the lookups.

  1. sign up at cloudflare, choose add a domain, and enter your domain. the free plan is enough.
  2. it scans your existing records and shows you the list. check your mail records before you continue — if you have email on this domain, the MX and TXT entries must all be there, or mail stops when the switch happens.
  3. cloudflare gives you two nameservers. change them at your registrar, where the domain is registered. this is the only step that happens outside cloudflare.
  4. wait for it to say active. usually minutes, occasionally a day — the old nameservers stay cached until they expire.
  5. then add a CNAME record for your bare domain pointing at the target shown in the page’s settings, and leave the proxy (the orange cloud) on. cloudflare flattens it for you.
you do not need any of this for a subdomain. if go.yourdomain.com is fine, add one CNAME wherever your dns already is and skip the move entirely.

don’t judge it by trying the url early

if you visit the address before the dns record exists, the failure is cached — by your browser, and by whichever resolver your network uses — for as long as the domain’s negative ttl says, commonly half an hour. the page can be live and your browser will still show an error. wait it out, or check from a phone on cellular, which shares no cache with your laptop.

what the states mean

waiting on dnsyoursthe record has not appeared yet. it is the one shown on the card, at your dns provider, exactly as written.
issuing certificateoursyour domain is verified and the certificate is being issued. usually a few minutes, and nothing is needed from you.
livedonethe domain serves the page over https.
failedstuckusually the hostname is already registered to another service on cloudflare. remove it there, then add it here again.

removing a domain stops it serving and deletes nothing — the page stays exactly where it was, at its htmlshare url.

one error shape everywhere.

09 / errors & limits
4xx / 5xx
{
"error": {
"code": "too_large",
"message": "File exceeds 10485760 bytes"
}
}
bad_request400malformed body or field
missing_file400no html or file sent
invalid_type400not html
unauthorized401bad key, or a token that page does not answer to
conflict409slug taken
too_large413over 10 mb
rate_limited429carries a retryAfter field and a Retry-After header
quota_exceeded413the site is full — delete a file, or the account needs a bigger allowance
upgrade_required402the plan refused it, not the request. not a 403: the message names the capability and where to upgrade

limits

anonymous
10 / 15 min
per ip, inside a 500 / 24h backstop. pages expire after 24 hours.
authenticated
1000 / 24h
counted against your account, not your ip. pages never expire.

rejected requests never count against a limit. uploads are scanned for phishing and malware; we never modify your html.