{"schema_version":"1","name":"htmlbin","public_url":"https://htmlbin.aesir.works","summary":"Agent-first HTML hosting. Drop self-contained HTML, get a public URL.","naming_convention":"snake_case for all request and response field names","spec":{"openapi":"https://htmlbin.aesir.works/openapi.json","agent_card":"https://htmlbin.aesir.works/.well-known/agent-card.json","api_catalog":"https://htmlbin.aesir.works/.well-known/api-catalog","agent_skills_index":"https://htmlbin.aesir.works/.well-known/agent-skills/index.json","llms_txt":"https://htmlbin.aesir.works/llms.txt","onboard_markdown":"https://htmlbin.aesir.works/api/onboard (Accept: text/markdown)","skill":"https://htmlbin.aesir.works/.well-known/agent-skills/htmlbin/SKILL.md"},"mcp":{"url":"https://htmlbin.aesir.works/mcp","transport":"streamable_http","method":"POST","auth":"Authorization: Bearer <api_token>","note":"Same hb_ token as the REST API. The human signs in with GitHub once at /verify, and the token from /api/auth/poll is the key. Send that header on every request, including initialize. This endpoint does not open a browser login.","tools":["whoami","create_drop","get_drop","list_drops","update_drop","patch_drop","delete_drop","list_versions","get_version","delete_version","set_passcode"]},"error_shape":{"description":"Every 4xx/5xx response uses this canonical shape. Switch on `code`, not on `message`.","example":{"error":{"code":"html_too_large","message":"HTML exceeds 2097152 bytes.","details":{"max_bytes":2097152}}}},"auth":{"type":"device_code","header":"Authorization: Bearer <token>","token_format":"hb_<base62>","token_storage":{"primary":"./.htmlbin/token","fallback":"~/.config/htmlbin/token","env_var":"HTMLBIN_TOKEN","note":"Project-local storage avoids prompting an agent to write outside its working directory."},"steps":[{"step":1,"method":"POST","url":"https://htmlbin.aesir.works/api/auth/start","body":{"label":"string (optional, e.g. 'claude-code')"},"returns":{"code":"string (show this to the human)","verification_url":"string (open this in a browser)","poll_token":"string (use in step 3)","expires_in":"integer (seconds)","poll_interval":"integer (seconds, default 2)"}},{"step":2,"human_action":"Open verification_url and sign in with GitHub.","note":"Only human moment. We bind one htmlbin account per GitHub identity (read:user only — public username + id), so quotas and existing drops follow the human across machines. After this the agent is autonomous."},{"step":3,"method":"GET","url":"https://htmlbin.aesir.works/api/auth/poll","query":{"token":"<poll_token from step 1>"},"returns":{"status":"'pending' | 'verified' | 'expired' | 'claimed'","api_token":"string (only on first 'verified' read; revealed exactly once)","user_id":"string (only on first 'verified' read)"},"note":"Poll every poll_interval seconds until status != 'pending'. The api_token is shown once — store it. Claim it within 10 minutes of the human signing in or the row expires and the token is discarded. An unknown poll token is a 404 with error.code 'not_found'."}]},"drop_shape":{"description":"Every endpoint that creates, reads, or mutates a single drop returns this shape.","example":{"slug":"aB3xK7gPq","title":"My page","description":"Optional subtitle","url":"https://htmlbin.aesir.works/p/aB3xK7gPq","raw_url":"https://htmlbin.aesir.works/p/aB3xK7gPq/raw","locked":false,"latest_version":3,"view_count":17,"metadata":{"repo":"u/r","pr":"42"},"created_at":0,"updated_at":0}},"publish":{"method":"POST","url":"https://htmlbin.aesir.works/api/drops","headers":{"Authorization":"Bearer <api_token>","Content-Type":"application/json"},"body":{"title":"string (required, ≤200 chars)","description":"string (optional, ≤500 chars)","html":"string (required, full self-contained HTML document, ≤2 MB)","passcode":"string (optional, ≥4 chars; soft gate, not encryption — shown on /p/<slug> before the body)","context":"string (optional, ≤64 KB; reasoning trace — opt-in per the human)","metadata":"object (optional, ≤10 keys; flat string→string. Owner-side tag bag for lookup via GET /api/drops?metadata.k=v. Not exposed on /p/.)"},"returns":"Drop (see drop_shape)","status":201},"iterate":{"description":"Two distinct operations on an existing drop: PUT for a new version, PATCH for metadata-only changes.","new_version":{"method":"PUT","url":"https://htmlbin.aesir.works/api/drops/<slug>","body":{"html":"string (required — PUT always mints a new version)","title":"string (optional, ≤200 chars)","description":"string (optional, ≤500 chars)","context":"string (optional, ≤64 KB)","metadata":"object (optional; replaces whole metadata map — omit to leave untouched, {} to clear)"},"returns":"Drop (with bumped latest_version)","note":"Slug + public URL stay stable. Humans switch versions in the viewer with ?v=N."},"metadata_only":{"method":"PATCH","url":"https://htmlbin.aesir.works/api/drops/<slug>","body":{"title":"string (optional, ≤200 chars)","description":"string (optional, ≤500 chars)","metadata":"object (optional; replaces whole metadata map — omit to leave untouched, {} to clear)"},"returns":"Drop (latest_version unchanged)","note":"Including `html` here returns 400 metadata_only_on_patch — use PUT."}},"list_my_drops":{"method":"GET","url":"https://htmlbin.aesir.works/api/drops","query":{"page":"integer (default 1, min 1)","pageSize":"integer (default 50, max 200)","sortBy":"'created_at' | 'updated_at' | 'view_count' (default 'created_at')","sortOrder":"'asc' | 'desc' (default 'desc')","metadata.<key>":"string (optional, repeatable; AND-filters drops where metadata.<key> = value. Conventional keys: repo, pr, sha, agent, ci_run.)"},"returns":{"data":"Drop[]","pagination":{"page":"integer","page_size":"integer","total_items":"integer","total_pages":"integer","sort_by":"string","sort_order":"string"}},"lookup_then_mutate":{"description":"Recipe for finding a drop you previously tagged and updating it without storing slugs client-side: GET filter, then PUT if a drop matches, otherwise POST. Works for any tag combination, not just CI / PR previews — the metadata field is free-form.","steps":["GET https://htmlbin.aesir.works/api/drops?metadata.<k1>=<v1>&metadata.<k2>=<v2>","if data[0]: PUT /api/drops/<data[0].slug> with the new html","else:       POST /api/drops with html + metadata"],"note":"There is no server-side upsert. Run the GET, then PUT or POST. The same two calls are the MCP tools list_drops, then update_drop or create_drop.","example_tag_setups":[{"repo":"u/r","pr":"42"},{"session_id":"<chat-id>","kind":"deck"},{"client":"acme","project":"rebrand","status":"draft"}],"race_note":"If two writers can run in parallel for the same tag combination, serialize them at the call site. For CI / PR previews specifically, set `concurrency: group:` on the GitHub Actions workflow. Other shapes (per-session, per-client) usually don't race."}},"other_endpoints":{"whoami":{"method":"GET","url":"https://htmlbin.aesir.works/api/me","returns":{"user_id":"string","created_at":"integer (unix ms)","drop_count":"integer","token":{"id":"string (12 hex)","label":"string|null","created_at":"integer","last_used_at":"integer|null"}}},"get_drop":{"method":"GET","url":"https://htmlbin.aesir.works/api/drops/<slug>"},"list_versions":{"method":"GET","url":"https://htmlbin.aesir.works/api/drops/<slug>/versions"},"get_version":{"method":"GET","url":"https://htmlbin.aesir.works/api/drops/<slug>/v/<n>"},"delete_drop":{"method":"DELETE","url":"https://htmlbin.aesir.works/api/drops/<slug>","returns":"204 No Content"},"delete_version":{"method":"DELETE","url":"https://htmlbin.aesir.works/api/drops/<slug>/v/<n>","returns":"Drop (with possibly-updated latest_version)","note":"Refused with 409 last_version_cannot_be_deleted on the only remaining version."},"set_passcode":{"method":"POST","url":"https://htmlbin.aesir.works/api/drops/<slug>/passcode","body":{"passcode":"string ('' to remove)"},"returns":"Drop"},"list_my_tokens":{"method":"GET","url":"https://htmlbin.aesir.works/api/tokens"},"revoke_token":{"method":"DELETE","url":"https://htmlbin.aesir.works/api/tokens/<id>","note":"id = first 12 hex chars of the token hash","returns":"204 No Content"}},"cross_machine":{"method":"On a new machine, run /api/auth/start and sign in with the same GitHub account at /verify. Both machines end up bound to the same htmlbin account.","result":"Both machines share the same user_id with independent tokens."},"limits":{"max_html_bytes":2097152,"max_context_bytes":65536,"max_versions_per_drop":200,"writes_per_minute":60,"writes_per_day":500,"write_limits_scope":"account","drops_per_account":500,"verification_ttl_seconds":600},"rate_limits":{"response":"429 with `error.code = rate_limited` (or daily_quota_exceeded / quota_exceeded)","retry_after_header":"Set to seconds until the next window opens.","retry_after_details":"Also returned as `details.retry_after_seconds` in the body."},"errors":{"shape":{"error":{"code":"<machine-readable identifier>","message":"<human-readable summary>","details":"<optional object with context, e.g. { max_bytes: 2097152 }>"}},"common_codes":["unauthorized","invalid_token","rate_limited","daily_quota_exceeded","quota_exceeded","html_too_large","html_required","title_required","title_too_long","description_too_long","context_too_large","passcode_required","passcode_too_short","metadata_only_on_patch","forbidden","not_found","version_not_found","last_version_cannot_be_deleted","version_limit_reached","version_conflict","invalid_slug","invalid_arg","invalid_json","token_required"]},"recommendations":{"mobile_floor":{"summary":"Render at 360px without horizontal scroll. This is the #1 failure mode for agent-authored drops — author mobile-first.","must_have":["<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"> in <head> (server injects if missing).","No fixed pixel widths >360px on layout containers. Use max-width paired with width: 100%.","Long unbreakable strings (URLs, paths, hashes) wrap: overflow-wrap: anywhere on prose, table cells, inline code.","<pre> and wide <table> scroll internally, not the page: overflow-x: auto; max-width: 100%.","Media is fluid: img, svg, video, iframe { max-width: 100% }.","Mentally test at 360px and 768px before publishing."],"server_safety_net":"Every /p/<slug>/raw response gets a small defensive style appended at the tail of <head>: html,body{max-width:100vw;overflow-x:clip}, plus fluid img/svg/video/iframe and max-width:100% on pre/table. It's a guard against the most common break, not a license to skip mobile design.","skill":"https://htmlbin.aesir.works/.well-known/agent-skills/htmlbin/SKILL.md"}},"notes":["Drops are rendered in an iframe — HTML must be standalone (CDN imports OK; no build step).","If the human's agent sandbox blocks new domains, allow htmlbin.aesir.works once before running this flow."]}