{"openapi":"3.1.0","info":{"title":"qbin API","version":"1","description":"Agent-first static HTML hosting. Publish one self-contained HTML file, get a permanent public URL with version history. Auth is a device-code flow over GitHub OAuth; tokens are hb_-prefixed bearer tokens. All errors share one shape — switch on error.code, never the message."},"servers":[{"url":"https://apps.qyvr.ai"}],"tags":[{"name":"auth","description":"Device-code auth over GitHub OAuth (public)."},{"name":"account","description":"Identity and ownership (bearer)."},{"name":"tokens","description":"API token management (bearer)."},{"name":"drops","description":"Publish, read, update, delete drops (bearer)."},{"name":"versions","description":"Version history per drop (bearer)."},{"name":"serving","description":"Public rendering of drops."}],"paths":{"/api/auth/start":{"post":{"tags":["auth"],"summary":"Start device auth","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","description":"Free-form label shown on the verify page, e.g. 'my-agent'."}}}}}},"responses":{"200":{"description":"Pending code issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthStart"},"example":{"code":"X7Q2-9MZD","verification_url":"https://apps.qyvr.ai/verify?code=X7Q2-9MZD","poll_token":"pt_abc123","expires_in":600,"poll_interval":2}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}}},"/api/auth/poll":{"get":{"tags":["auth"],"summary":"Poll for token issuance","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"poll_token from /api/auth/start."}],"responses":{"200":{"description":"Current code status. On the first verified read the api_token is returned in plaintext exactly once, then the code is claimed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthPoll"},"examples":{"pending":{"value":{"status":"pending"}},"verified":{"value":{"status":"verified","api_token":"hb_...","user_id":"u_abc"}}}}}}}}},"/verify":{"get":{"tags":["auth"],"summary":"Human verification page","description":"The human opens verification_url here, clicks Sign in with GitHub, and the code is marked verified.","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"HTML verify page.","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/auth/github/authorize":{"get":{"tags":["auth"],"summary":"Begin GitHub OAuth (human)","description":"302 redirect to github.com/login/oauth/authorize with state=<code>.","responses":{"302":{"description":"Redirect to GitHub."}}}},"/api/auth/github/callback":{"get":{"tags":["auth"],"summary":"GitHub OAuth callback (human)","description":"Exchanges the code, upserts the user by GitHub id, marks the device code verified, shows a confirmation page.","parameters":[{"name":"code","in":"query","required":true,"schema":{"type":"string"}},{"name":"state","in":"query","required":true,"schema":{"type":"string","description":"The device code."}}],"responses":{"200":{"description":"HTML confirmation page.","content":{"text/html":{"schema":{"type":"string"}}}},"400":{"description":"Bad or expired OAuth state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"invalid_arg","message":"Bad or expired OAuth state."}}}}}}}},"/api/me":{"get":{"tags":["account"],"summary":"Identity and quota snapshot","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"User info plus the calling token's metadata.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}}}}},"/api/tokens":{"get":{"tags":["tokens"],"summary":"List API tokens","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tokens owned by the caller (hashes never exposed).","content":{"application/json":{"schema":{"type":"object","required":["tokens"],"properties":{"tokens":{"type":"array","items":{"$ref":"#/components/schemas/TokenInfo"}}}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}}}}},"/api/tokens/{id}":{"delete":{"tags":["tokens"],"summary":"Revoke a token","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Revoked."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}}}}},"/api/drops":{"get":{"tags":["drops"],"summary":"List drops","security":[{"bearerAuth":[]}],"parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":20}},{"name":"sortBy","in":"query","schema":{"type":"string","enum":["created_at","updated_at","view_count"],"default":"created_at"}},{"name":"sortOrder","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"metadata.<key>","in":"query","schema":{"type":"string"},"description":"Repeatable metadata filter, AND across pairs, e.g. ?metadata.project=acme&metadata.status=draft."}],"responses":{"200":{"description":"Paginated drops.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropList"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}},"post":{"tags":["drops"],"summary":"Publish a drop","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishDrop"}}}},"responses":{"201":{"description":"Created. Returns the full Drop object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"html_required","message":"Validation failed."}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}}},"/api/drops/{slug}":{"get":{"tags":["drops"],"summary":"Get a drop","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"responses":{"200":{"description":"The Drop object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}}}},"put":{"tags":["drops"],"summary":"Publish a new version","description":"html is required; mints version n+1. The slug never changes.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["html"],"properties":{"html":{"type":"string","description":"Complete HTML document, ≤ 2,097,152 bytes."},"context":{"type":"string","description":"Optional ≤64KB opt-in context.","maxLength":65536}}}}}},"responses":{"200":{"description":"Updated Drop object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"html_required","message":"Validation failed."}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}},"patch":{"tags":["drops"],"summary":"Edit title/description/metadata without a new version","description":"Including html returns 400 metadata_only_on_patch.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":500},"metadata":{"$ref":"#/components/schemas/Metadata"}}}}}},"responses":{"200":{"description":"Updated Drop object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"metadata_only_on_patch","message":"Validation failed."}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}},"delete":{"tags":["drops"],"summary":"Delete a drop","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}}},"/api/drops/{slug}/versions":{"get":{"tags":["versions"],"summary":"List versions","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"responses":{"200":{"description":"Versions, latest first.","content":{"application/json":{"schema":{"type":"object","required":["versions"],"properties":{"versions":{"type":"array","items":{"$ref":"#/components/schemas/Version"}}}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}}}}},"/api/drops/{slug}/v/{n}":{"get":{"tags":["versions"],"summary":"Get one version","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."},{"name":"n","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"The Version object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Version"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such drop or version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such drop or version."}}}}}}},"delete":{"tags":["versions"],"summary":"Delete one version","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."},{"name":"n","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"204":{"description":"Deleted."},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such drop or version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such drop or version."}}}}},"409":{"description":"Refused: it is the last remaining version.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"last_version_cannot_be_deleted","message":"Refused: it is the last remaining version."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}}},"/api/drops/{slug}/passcode":{"post":{"tags":["drops"],"summary":"Set or remove the passcode gate","description":"Empty string removes the gate. Under 4 chars → 400. A soft share gate, not encryption.","security":[{"bearerAuth":[]}],"parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["passcode"],"properties":{"passcode":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated Drop object.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"}}}},"400":{"description":"Validation failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"passcode_required","message":"Validation failed."}}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"unauthorized","message":"Missing or invalid bearer token."}}}}},"404":{"description":"No such resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"not_found","message":"No such resource."}}}}},"429":{"description":"Rate limited. Carries a Retry-After header and details.retry_after_seconds.","headers":{"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":{"code":"rate_limited","message":"Too many requests.","details":{"retry_after_seconds":42}}}}}}}}},"/p/{slug}":{"get":{"tags":["serving"],"summary":"Viewer shell (public)","description":"Server-rendered shell with title, version picker, and the drop in a sandboxed iframe. Passcode-gated drops render a gate page first. ?v=N pins a version.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."},{"name":"v","in":"query","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"HTML viewer.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Styled 404 page."}}}},"/p/{slug}/raw":{"get":{"tags":["serving"],"summary":"Raw drop HTML (public)","description":"The drop HTML verbatim plus a safety-net <style>. ?v=N pins a version.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9]{7}$"},"description":"Immutable 7-char base62 drop id."},{"name":"v","in":"query","schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"text/html.","content":{"text/html":{"schema":{"type":"string"}}}},"404":{"description":"Styled 404 page."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"API token, hb_-prefixed. Send as: Authorization: Bearer hb_..."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["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"],"description":"Machine code — switch on this, never the message."},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true}}}}},"Metadata":{"type":"object","description":"Owner-side tag bag. Never rendered publicly. Omitted → untouched, {} → cleared, map → replaced.","maxProperties":10,"propertyNames":{"pattern":"^[a-z0-9_]([a-z0-9_.-]{0,62}[a-z0-9_])?$"},"additionalProperties":{"type":"string","maxLength":256}},"Drop":{"type":"object","required":["slug","url","raw_url","latest_version","view_count","metadata","created_at","updated_at"],"properties":{"slug":{"type":"string","pattern":"^[A-Za-z0-9]{7}$","description":"Immutable 7-char base62 id."},"title":{"type":"string","maxLength":200},"description":{"type":["string","null"],"maxLength":500},"url":{"type":"string","format":"uri","description":"Public viewer URL."},"raw_url":{"type":"string","format":"uri","description":"Raw HTML URL."},"locked":{"type":"boolean","description":"True when a passcode gate is set."},"latest_version":{"type":"integer","minimum":1},"view_count":{"type":"integer","minimum":0},"metadata":{"$ref":"#/components/schemas/Metadata"},"created_at":{"type":"integer","description":"Unix milliseconds."},"updated_at":{"type":"integer","description":"Unix milliseconds."}}},"Version":{"type":"object","required":["slug","version","size_bytes","has_context","created_at"],"properties":{"slug":{"type":"string"},"version":{"type":"integer","minimum":1},"size_bytes":{"type":"integer","minimum":0},"has_context":{"type":"boolean"},"created_at":{"type":"integer","description":"Unix milliseconds."}}},"TokenInfo":{"type":"object","required":["id","created_at","last_used_at"],"properties":{"id":{"type":"string"},"label":{"type":["string","null"]},"created_at":{"type":"integer","description":"Unix milliseconds."},"last_used_at":{"type":"integer","description":"Unix milliseconds."}}},"PublishDrop":{"type":"object","required":["html","title"],"properties":{"html":{"type":"string","description":"Complete HTML document, ≤ 2,097,152 bytes.","maxLength":2097152},"title":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":500},"passcode":{"type":"string","description":"≥ 4 chars. Soft share gate, not encryption."},"context":{"type":"string","description":"Opt-in ≤64KB context.","maxLength":65536},"metadata":{"$ref":"#/components/schemas/Metadata"}}},"DropList":{"type":"object","required":["drops","page","page_size","total"],"properties":{"drops":{"type":"array","items":{"$ref":"#/components/schemas/Drop"}},"page":{"type":"integer"},"page_size":{"type":"integer"},"total":{"type":"integer"}}},"AuthStart":{"type":"object","required":["code","verification_url","poll_token","expires_in","poll_interval"],"properties":{"code":{"type":"string","description":"Human-friendly code, XXXX-XXXX."},"verification_url":{"type":"string","format":"uri"},"poll_token":{"type":"string"},"expires_in":{"type":"integer","description":"Seconds (600)."},"poll_interval":{"type":"integer","description":"Seconds between polls (2)."}}},"AuthPoll":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["pending","verified","expired","claimed","not_found"]},"api_token":{"type":"string","description":"Present only on the first verified read — plaintext, revealed exactly once."},"user_id":{"type":"string","description":"Present on verified reads."}}},"Me":{"type":"object","required":["user_id","created_at","drop_count","token"],"properties":{"user_id":{"type":"string"},"created_at":{"type":"integer","description":"Unix milliseconds."},"drop_count":{"type":"integer"},"token":{"$ref":"#/components/schemas/TokenInfo"}}}}}}