the whole api,
one page.
htmlshare turns an html document into a public url. one endpoint to publish, one mcp server for agents.
publish in one request.
01 / quickstartcurl -X POST https://api.htmlshare.net/v1/publish \-H "content-type: application/json" \-d '{"html":"<!doctype html><h1>hello</h1>"}'
{"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. ..."}}
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.
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 apiapplication/json with an html string fieldmultipart/form-data with a file field — text/html, or ending in .html / .htm409 conflictwith an api key, publish at a memorable url under your claimed prefix:
curl -X POST https://api.htmlshare.net/v1/publish \-H "authorization: Bearer ps_live_..." \-F "[email protected]" \-F "slug=q3-report"
{"siteId": "x7p4nn21","url": "https://htmlshare.net/@yourprefix/q3-report","expiresAt": null,"title": "Q3 Report"}
the manage token.
03 / manage a pagean 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.
editing beats republishing. a second publish mints a second url and orphans the first — the one thing a link you already sent cannot survive.
# 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 urlcurl -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 base64curl -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 leftcurl 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]"}'
update the document
upload, list, delete its files
attach a reminder email
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
410.404 — that is the page changing hands, not vanishing. a lost token has no recovery path.bearer keys.
04 / api keysPOST /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 use401, not an anonymous fallbackwhat 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_entitlements, a2a method htmlshare/entitlements. both flag the capabilities the plan grants that nothing implements yet402 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 workpublish from claude or chatgpt.
05 / mcp connectorconnect it and ask your agent to “publish this as an htmlshare link”. streamable http.
ps_live_ key works too.manageToken, nothing to claim. publishing without an account is still one POST away over rest.tools
publish_htmlsiteId.get_htmlselector.patch_htmlupdate_htmlset_slughtmlshare.net/@{prefix}/{slug}, or clears it. no redirects — the old url stops working.list_sitespublish_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_siteget_entitlementsevery 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_assetput_asset_from_urlcreate_uploadcurl 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).
claude mcp add --transport http htmlshare https://api.htmlshare.net/v1/mcp
{"mcpServers": {"htmlshare": {"url": "https://api.htmlshare.net/v1/mcp"}}}
desktop app: plugins → MCPs → connect to a custom MCP. name it htmlshare, switch type from STDIO to Streamable HTTP (this swaps the command field for a url field), url https://api.htmlshare.net/v1/mcp.
a credential is required, so add a header Authorization: Bearer ps_live_..., or use the bearer token env var field, which takes the name of a local env var holding the key. on the web, enable developer mode first (settings → security and login), add the same url as a custom connection from the plugins page, then enable it from the tools menu in a new chat.
settings → connectors → add custom connector (or the connectors panel in a chat). name it htmlshare, url https://api.htmlshare.net/v1/mcp. leave the client id blank — grok registers itself with us automatically.
it opens our sign-in, you approve the connection, and it is connected to that account. if you are already signed in to htmlshare in another tab it picks that account up and goes straight to the approval step.
for agents that read specs.
07 / openapithe 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
servers entry, so calls go to api.htmlshare.net.the json operations an action can drive:
publishHtmlgetSiteContentupdateSiteContentsetSiteSluglistSiteAssetsreportAbusegetHealthuploadSiteAsset 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.
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/llms.txt/auth.md/.well-known/agent-card.jsonPOST /v1/a2a publishes over the a2a json-rpc binding with the same one skill the card advertises.your page, your domain.
08 / custom domainsa 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.
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.
- sign up at cloudflare, choose add a domain, and enter your domain. the free plan is enough.
- 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
MXandTXTentries must all be there, or mail stops when the switch happens. - cloudflare gives you two nameservers. change them at your registrar, where the domain is registered. this is the only step that happens outside cloudflare.
- wait for it to say active. usually minutes, occasionally a day — the old nameservers stay cached until they expire.
- then add a
CNAMErecord 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.
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{"error": {"code": "too_large","message": "File exceeds 10485760 bytes"}}
bad_request400malformed body or fieldmissing_file400no html or file sentinvalid_type400not htmlunauthorized401bad key, or a token that page does not answer toconflict409slug takentoo_large413over 10 mbrate_limited429carries a retryAfter field and a Retry-After headerquota_exceeded413the site is full — delete a file, or the account needs a bigger allowanceupgrade_required402the plan refused it, not the request. not a 403: the message names the capability and where to upgradelimits
rejected requests never count against a limit. uploads are scanned for phishing and malware; we never modify your html.
social cards.
06 / social cardsa 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>before the first request.cache-control: immutableand anetag, so crawlers are answered at the edge. rate limited per ip.