{"schema_version":"1","name":"qbin","public_url":"https://apps.qyvr.ai","summary":"Agent-first static HTML hosting. An agent publishes one self-contained HTML file and gets back a permanent public URL with version history — no dashboard, no signup beyond a one-time GitHub sign-in.","naming_convention":"snake_case for all request and response field names","quickstart":[{"step":1,"action":"start device auth","method":"POST","url":"https://apps.qyvr.ai/api/auth/start","body":{"label":"optional string, e.g. 'my-agent'"},"returns":["code","verification_url","poll_token","expires_in","poll_interval"]},{"step":2,"action":"human opens verification_url and signs in with GitHub once","note":"Read-only identity (username + id). One qbin account per GitHub identity, so quotas and drops follow the human across machines."},{"step":3,"action":"poll for the token","method":"GET","url":"https://apps.qyvr.ai/api/auth/poll?token=<poll_token>","note":"Poll every poll_interval seconds. On the first 'verified' read the api_token is returned in plaintext exactly once, then the code is claimed."},{"step":4,"action":"publish","method":"POST","url":"https://apps.qyvr.ai/api/drops","headers":{"Authorization":"Bearer hb_..."},"body":{"title":"Page title (required, ≤200 chars)","html":"Complete HTML document (required, ≤2MB)"},"returns":"the Drop object — hand its url field back to the human"}],"auth":{"type":"device_code","human_step":"Exactly one: open verification_url, sign in with GitHub.","token":{"prefix":"hb_","header":"Authorization: Bearer hb_...","revealed":"plaintext, exactly once, on first verified poll","storage":"server stores sha256(token) only"},"code_format":"XXXX-XXXX","code_ttl_seconds":600,"poll_interval_seconds":2,"poll_statuses":["pending","verified","expired","claimed","not_found"]},"publish":{"method":"POST","url":"https://apps.qyvr.ai/api/drops","success_status":201,"fields":{"html":"required — complete HTML document, ≤ 2,097,152 bytes","title":"required — ≤ 200 chars","description":"optional — ≤ 500 chars, shown in the viewer shell","passcode":"optional — ≥ 4 chars; a soft share gate, not encryption","context":"optional — ≤ 64KB; opt-in only, may carry prompt content, include only with human agreement","metadata":"optional — ≤ 10 string→string pairs; owner-side tags, never shown publicly"},"html_guidance":"All CSS in <style>, all JS in <script>; self-contained preferred (no external JS bundles or images at the public URL). Mobile-OK at 360px: single column, no fixed pixel widths, long strings wrap."},"iterate":{"new_version":{"method":"PUT","url":"https://apps.qyvr.ai/api/drops/:slug","note":"html required; mints version n+1. The slug never changes across versions."},"metadata_only":{"method":"PATCH","url":"https://apps.qyvr.ai/api/drops/:slug","note":"title/description/metadata only — including html returns 400 metadata_only_on_patch."}},"versions":{"list":"GET /api/drops/:slug/versions","fetch_one":"GET /api/drops/:slug/v/:n","delete_one":"DELETE /api/drops/:slug/v/:n — refused on the last remaining version (409 last_version_cannot_be_deleted)","pin_in_viewer":"append ?v=N to /p/:slug or /p/:slug/raw to pin an older version","limit":"200 versions per drop (version_limit_reached)"},"passcode":{"set":"POST /api/drops/:slug/passcode {passcode} — empty string removes it; under 4 chars → 400 passcode_too_short","behavior":"The viewer renders a gate page until the correct passcode is entered (HttpOnly cookie, 7 days). A soft share gate, not encryption — stated plainly."},"metadata":{"shape":"flat object of string → string","key_format":"^[a-z0-9_]([a-z0-9_.-]{0,62}[a-z0-9_])?$","value_limit":"≤ 256 chars","max_keys":10,"visibility":"owner-only — never rendered in the public viewer","replace_semantics":"key omitted → untouched; {} → cleared; map → replaced","filter":"GET /api/drops?metadata.<k>=<v> — repeatable, AND across pairs","recipe_lookup_mutate":"GET /api/drops with metadata filters → PUT /api/drops/:slug when a drop matches, else POST a fresh one. Keeps one stable URL across pushes (e.g. per-PR previews)."},"serving":{"viewer":"https://apps.qyvr.ai/p/:slug — server-rendered shell (title, version picker, context toggle) with the drop inside a sandboxed iframe (allow-scripts allow-forms allow-modals allow-downloads; opaque origin, so storage APIs are unavailable in the viewer)","raw":"https://apps.qyvr.ai/p/:slug/raw — the drop HTML served verbatim (plus a small safety-net <style>); storage APIs work here, but the origin is shared across all drops, so namespace keys and never store secrets","pinned":"?v=N pins an older version on both URLs","raw_headers":"Content-Type text/html; CSP frame-ancestors 'self', base-uri 'none', form-action 'self' https:; Permissions-Policy geolocation=(), camera=(), microphone=(), payment=(), usb=(); X-Robots-Tag: noindex"},"limits":{"max_html_bytes":2097152,"title":"required, ≤ 200 chars","description":"≤ 500 chars","passcode":"≥ 4 chars","context":"≤ 64KB, opt-in","metadata":"≤ 10 keys, values ≤ 256 chars","versions_per_drop":200,"drops_per_account":500},"rate_limits":{"note":"429 responses carry a Retry-After header and details.retry_after_seconds","writes_per_minute_per_token":60,"writes_per_day_per_token":500},"quotas":{"drops_per_account":500,"versions_per_drop":200},"errors":{"shape":{"error":{"code":"string","message":"string","details?":"object"}},"rule":"Switch on error.code, never on error.message.","codes":["unauthorized","invalid_token","token_required","forbidden","not_found","version_not_found","invalid_slug","invalid_arg","invalid_json","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","last_version_cannot_be_deleted","version_limit_reached","rate_limited","daily_quota_exceeded","quota_exceeded","expired_code"]},"cli":{"note":"No first-party CLI in v1 — qbin implements the cloud wire protocol, so existing tooling works via a base-URL override.","example":"In ./.htmlbin/config (TOML): api_url = \"https://apps.qyvr.ai\" — then login, publish, update, list, and delete behave as usual. Tokens are hb_-prefixed."},"spec":{"openapi":"https://apps.qyvr.ai/openapi.json","agent_card":"https://apps.qyvr.ai/.well-known/agent-card.json","api_catalog":"https://apps.qyvr.ai/.well-known/api-catalog","agent_skills_index":"https://apps.qyvr.ai/.well-known/agent-skills/index.json","patterns_index":"https://apps.qyvr.ai/.well-known/patterns/index.json","llms_txt":"https://apps.qyvr.ai/llms.txt"}}