{"info":{"title":"hank-scholar","version":"0.1.0","description":"Sourced medical-coding answers over a unified **chat + MCP** API - every claim cites its source.\n\n**Auth:** click **Authorize** and paste a `console.hank.ai` bearer token. Then call the OpenAI-compatible `POST /{tenant}/v1/chat/completions` endpoint (the live hub tenant is `hank`), or connect an agent over MCP at `/mcp/{tenant}/` - full per-agent setup (Claude Code, Claude Desktop, Cursor, ChatGPT, …) is at **/{tenant}/connect**.\n\nThe default server below is the metered Hank edge (`api.hank.ai/v1/scholar`); requests there are billed to your console account."},"paths":{"/healthz":{"get":{"summary":"Healthz","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"}},"operationId":"healthz_healthz_get"}},"/embed.js":{"get":{"summary":"Embed Js","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"}},"description":"Serve the vanilla JS loader. Phase 1 fills brand defaults at request\ntime so embedders can drop the script tag without configuring color;\n``data-bg-color`` / ``data-logo-url`` attributes override.\n\nD5-2: default logo URL is the AAPC R2-resolved path - AAPC drops the\nscript tag without ``data-logo-url`` and the brand pill renders.\nOther tenants override via ``data-logo-url`` on their ``<script>`` tag.","operationId":"embed_js_embed_js_get"}},"/{tenant}/":{"get":{"summary":"Tenant Index","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"query","name":"embed","schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Embed"},"required":false},{"in":"query","name":"strip","schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strip"},"required":false}],"description":"Render the SPA shell HTML.\n\nBehavior:\n  * Tenant must be loaded into ``app.state.tenants`` - otherwise 404.\n  * In embed mode (``?embed=1``) the response carries a\n    ``Content-Security-Policy: frame-ancestors <derived>`` header where\n    ``<derived>`` is ``surfaces.embed.csp_frame_ancestors`` from the\n    tenant's YAML. The earlier code-review fix mandates apex inclusion\n    (e.g. ``https://aapc.com https://*.aapc.com``).\n\nPhase 1 stub posture: the body is minimal HTML with the right meta\ntags + ``#app`` mount point + ``data-tenant`` / ``data-embed`` /\n``data-strip`` / ``data-bearer`` attributes. Day-5 Task 5.1 produces\nthe real Vite-built React bundle that mounts into ``#app``.\n\nW-14 fix: emit ``data-bearer`` so Chat.tsx reads the token from the\nDOM instead of hardcoding ``hk_aapc_live_demo``. Phase 1: bearer is\nthe first ``api_key`` from ``tctx.auth['api_keys']`` (env-resolved);\nPhase 2 will issue a JWT here from the SSO session.\n\nD5-10 fix: ``?strip=1`` forces neutral-mode rendering for the\nB2B-agent surface even when the tenant's ``b2b_agent.branding.strip``\nis false. ``data-strip=\"1\"`` propagates the flag to App.tsx which\napplies the strip palette.\n\nD5-12 fix: head injects Google Fonts preconnect + a per-tenant\n``<link>`` for the configured ``font_pair``. This eliminates the\ncold-cache FOUT on first render - ``applyTokens`` still injects on\nhydration as a defense in depth, but the preload starts sooner via\nthe ``<head>`` tags.","operationId":"tenant_index__tenant___get"}},"/{tenant}/v1/usage":{"get":{"summary":"Get Usage","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: ``{\"total_usd\": float, \"queries\": int}``.\n\n``require_tenant_match`` also runs ``authenticate``; the returned ``User``\nhas its tenant binding already validated against the path-tenant param.","operationId":"get_usage__tenant__v1_usage_get"}},"/{tenant}/v1/models":{"get":{"summary":"List Models","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: {\"object\": \"list\", \"data\": [{id, object, backend, status,\ndisplay_name, tagline}, ...]}.\n\n``status`` is always ``\"available\"`` in Phase 1 - Day 5 wires a real probe\nfor ``local/*`` models against MLX_SERVER_URL so the picker can degrade\nthem to ``\"unavailable\"`` when the local server is down.","operationId":"list_models__tenant__v1_models_get"}},"/{tenant}/v1/styles":{"get":{"summary":"List Styles","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: {\"object\": \"list\", \"data\": [{id, display_name, tagline,\ndefault}, ...], \"default\": <id|None>} for the StylePicker.\n\nLike /v1/models this is tenant-scoped (a bearer can't enumerate another\ntenant's styles). The per-style ``directive`` is the system-prompt block and\nis DELIBERATELY omitted - it's prompt-engineering, not UI copy, and we don't\nhand the model's instructions to the browser. An empty ``data`` list tells\nthe UI to hide the picker (feature off for this tenant).","operationId":"list_styles__tenant__v1_styles_get"}},"/{tenant}/theme.json":{"get":{"summary":"Get Theme","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns a flat dict of semantic_tokens + branding fields. Schema is\nstable; the SPA `theme.ts` consumer ignores unknown keys, so adding new\nsurface-level flags here is forward-compatible.","operationId":"get_theme__tenant__theme_json_get"}},"/{tenant}/v1/sources":{"get":{"summary":"List Sources","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: ``{\"object\": \"list\", \"data\": [{id, type}, ...]}``.\n\n``type`` reflects the ingestion source flavor (firecrawl, local_fs, etc).\n``license_approval_ref`` is intentionally omitted (internal audit field - see\nthe module docstring).","operationId":"list_sources__tenant__v1_sources_get"}},"/{tenant}/v1/feedback":{"post":{"summary":"Post Feedback","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: ``{\"ok\": True}`` on success.\n\nThe ``tenants.id`` lookup is intentionally a separate query (cleaner than\na subselect against a tenant-scoped table - the ``tenants`` table itself\nis not RLS-protected). ``tenant_conn_dep`` has already verified the slug\nexists, so this lookup never returns None at runtime.","operationId":"post_feedback__tenant__v1_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackPayload"}}},"required":true}}},"/{tenant}/v1/corpus-info":{"get":{"summary":"Get Corpus Info","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: ``{\n    doc_count: int,                             # documents in this tenant's corpus\n    chunk_count: int,                           # total chunks\n    last_refreshed_at: ISO 8601 str | None,     # ingestion_runs.finished_at max\n    source_summary: [{ id, type, doc_count }],  # per-source breakdown\n    display_name: str,                          # tenant's product_name\n}``","operationId":"get_corpus_info__tenant__v1_corpus_info_get"}},"/{tenant}/v1/codesets-diag":{"get":{"summary":"Codesets Diag","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"query","name":"code","schema":{"type":"string","title":"Code","default":"27447"},"required":false},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Diagnostic (bearer-gated): runs the HANK Codesets descriptor lookup from\nthe server and returns the raw outcome - configured base URL, the exact\nrequest URL, HTTP status + body, or the connection exception. Lets us debug\nwhy the CPT descriptor isn't resolving (DNS vs port vs 401 vs path) without\nreading Railway logs. Does NOT expose the API key. Safe to remove once the\nCodesets integration is confirmed working.","operationId":"codesets_diag__tenant__v1_codesets_diag_get"}},"/mcp/{tenant}/manifest.json":{"get":{"summary":"Manifest","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true}],"description":"Manifest shape per the MCP convention: name, version, description,\ntools (name + description), transport, endpoint, auth.\n\n404s if the tenant slug isn't loaded into ``app.state.tenants`` - the\nsame shape ``/{tenant}/v1/models`` and friends return for unknown tenants\nso error envelopes stay uniform across the surface.","operationId":"manifest_mcp__tenant__manifest_json_get"}},"/{tenant}/v1/research-depths":{"get":{"summary":"List Research Depths","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"Returns: {\"object\": \"list\", \"data\": [{id, display_name, tagline,\ndefault}, ...], \"default\": <id|None>} for the ResearchDepthPicker.\n\nTenant-scoped like /v1/models and /v1/styles (a bearer can't enumerate\nanother tenant's presets). Each preset's ``max_hops`` + ``loop_budget_s``\nare DELIBERATELY omitted - they're server loop-tuning knobs, not UI copy,\nexactly as list_styles withholds the ``directive``. An empty ``data`` list\ntells the UI to hide the picker (feature off for this tenant, which keeps\nrunning on retrieval.agentic_max_hops / agentic_loop_budget_s).","operationId":"list_research_depths__tenant__v1_research_depths_get"}},"/{tenant}/v1/chat/completions":{"post":{"summary":"Chat Completions","responses":{"200":{"content":{"application/json":{"schema":{}}},"description":"Successful Response"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}},"description":"Validation Error"}},"parameters":[{"in":"path","name":"tenant","schema":{"type":"string","title":"Tenant"},"required":true},{"in":"header","name":"authorization","schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Authorization"},"required":false}],"description":"OpenAI-compatible chat completions over a tenant's licensed corpus, with\nsourced citations.\n\nRequest body is OpenAI-shaped (`model`, `messages`) plus two Hank extensions:\n  * `stream` (bool, default true) - set false for a single blocking JSON body.\n  * `mode` (`'ai' | 'search'`, default `'ai'`) - `'search'` returns cited\n    source chunks only (retrieval, no LLM generation or cost).\n\nResponses are OpenAI `chat.completion.chunk` frames plus Hank SSE extension\nevents (`hank.citations`, `hank.usage`, `hank.cpt_fallback`). Authenticated\nwith a Hank API token bound to the tenant in the path.","operationId":"chat_completions__tenant__v1_chat_completions_post"}}},"openapi":"3.1.0","servers":[{"url":"https://api.hank.ai/v1/scholar","description":"Hank edge - metered; auth with your console.hank.ai bearer"},{"url":"/","description":"this origin (direct; SSO cookie or tagged bearer)"}],"security":[{"BearerAuth":[]}],"components":{"schemas":{"FeedbackPayload":{"type":"object","title":"FeedbackPayload","required":["request_id","rating"],"properties":{"rating":{"type":"integer","title":"Rating","maximum":1,"minimum":-1},"comment":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Comment"},"request_id":{"type":"string","title":"Request Id","maxLength":128}}},"ValidationError":{"type":"object","title":"ValidationError","required":["loc","msg","type"],"properties":{"ctx":{"type":"object","title":"Context"},"loc":{"type":"array","items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"}}},"HTTPValidationError":{"type":"object","title":"HTTPValidationError","properties":{"detail":{"type":"array","items":{"$ref":"#/components/schemas/ValidationError"},"title":"Detail"}}}},"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Your Hank API token from console.hank.ai, sent as `Authorization: Bearer <token>`."}}}}